Quellcode durchsuchen

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 vor 2 Monaten
Ursprung
Commit
a624e0e698

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

@@ -4,7 +4,7 @@
 //
 //  Template gallery for the CV Maker sidebar destination. Light-theme rendering
 //  inspired by a dark reference UI: page header, category toggle, style chips,
-//  4-column thumbnail grid with hover Preview overlay, and a sticky bottom CTA.
+//  4-column thumbnail grid and a sticky bottom CTA.
 //
 
 import Cocoa
@@ -512,7 +512,6 @@ final class CVMakerPageView: NSView {
         static let ctaBackground = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 1)
         static let ctaHover = NSColor(srgbRed: 28 / 255, green: 70 / 255, blue: 140 / 255, alpha: 1)
         static let ctaText = NSColor.white
-        static let overlayTint = NSColor.black.withAlphaComponent(0.45)
         static let selectionGlow = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 0.55)
         static let gradientTop = NSColor(srgbRed: 250 / 255, green: 252 / 255, blue: 1, alpha: 1)
         static let gradientBottom = NSColor(srgbRed: 236 / 255, green: 244 / 255, blue: 1, alpha: 1)
@@ -534,11 +533,14 @@ final class CVMakerPageView: NSView {
     private var selectedGroup: CVCategoryGroup = .designBased
     private var selectedFamily: CVDesignFamily? = nil // nil == "All"
     private var selectedTemplateID: String? = "paper-white"
+    /// Exactly one gallery card — avoids multiple highlighted cards when catalog entries share the same `template.id`.
+    private var selectedTemplateCardToken: UUID?
     /// Shown immediately; replaced when `CVTemplateFetchService` returns AI-generated entries.
     private var activeCatalog: [CVTemplate] = CVTemplateCatalog.all
     private var groupTabButtons: [CVCategoryGroup: CVChipButton] = [:]
     private var familyChipButtons: [CVDesignFamily?: CVChipButton] = [:]
-    private var templateCardsByID: [String: CVTemplateCard] = [:]
+    /// Every visible gallery card (not keyed by id — duplicate AI ids would collapse in a dictionary and break single-selection visuals).
+    private var templateCardsInGrid: [CVTemplateCard] = []
 
     private var appliedGridColumnCount: Int = 0
 
@@ -764,7 +766,7 @@ final class CVMakerPageView: NSView {
             gridStack.removeArrangedSubview($0)
             $0.removeFromSuperview()
         }
-        templateCardsByID.removeAll()
+        templateCardsInGrid.removeAll()
 
         let templates = visibleTemplates
         if templates.isEmpty {
@@ -772,6 +774,7 @@ final class CVMakerPageView: NSView {
             empty.font = .systemFont(ofSize: 13)
             empty.textColor = Palette.secondaryText
             gridStack.addArrangedSubview(empty)
+            selectedTemplateCardToken = nil
             return
         }
 
@@ -792,10 +795,12 @@ final class CVMakerPageView: NSView {
                     let template = templates[position]
                     let card = CVTemplateCard(template: template, palette: palette())
                     card.translatesAutoresizingMaskIntoConstraints = false
-                    card.onSelect = { [weak self] in self?.didSelectTemplate(template.id) }
-                    card.onPreview = { [weak self] in self?.didPreviewTemplate(template.id) }
+                    card.onSelect = { [weak self] in
+                        guard let self else { return }
+                        self.didSelectCard(card)
+                    }
                     row.addArrangedSubview(card)
-                    templateCardsByID[template.id] = card
+                    templateCardsInGrid.append(card)
                 } else {
                     let filler = NSView()
                     filler.translatesAutoresizingMaskIntoConstraints = false
@@ -807,8 +812,15 @@ final class CVMakerPageView: NSView {
             index += columns
         }
 
-        if selectedTemplateID == nil || templateCardsByID[selectedTemplateID ?? ""] == nil {
-            selectedTemplateID = templates.first?.id
+        if let sid = selectedTemplateID,
+           let match = templateCardsInGrid.first(where: { $0.templateID == sid }) {
+            selectedTemplateCardToken = match.selectionToken
+        } else if let first = templateCardsInGrid.first {
+            selectedTemplateCardToken = first.selectionToken
+            selectedTemplateID = first.templateID
+        } else {
+            selectedTemplateCardToken = nil
+            selectedTemplateID = nil
         }
         applySelectionToCards()
     }
@@ -835,8 +847,9 @@ final class CVMakerPageView: NSView {
     }
 
     private func applySelectionToCards() {
-        for (id, card) in templateCardsByID {
-            card.isSelected = (id == selectedTemplateID)
+        let token = selectedTemplateCardToken
+        for card in templateCardsInGrid {
+            card.isSelected = (card.selectionToken == token)
         }
     }
 
@@ -855,8 +868,7 @@ final class CVMakerPageView: NSView {
             previewAccentRed: Palette.previewAccentRed,
             previewAccentBlue: Palette.previewAccentBlue,
             primaryText: Palette.primaryText,
-            secondaryText: Palette.secondaryText,
-            overlayTint: Palette.overlayTint
+            secondaryText: Palette.secondaryText
         )
     }
 
@@ -878,21 +890,12 @@ final class CVMakerPageView: NSView {
         updateSelectedChipStates()
     }
 
-    private func didSelectTemplate(_ id: String) {
-        selectedTemplateID = id
+    private func didSelectCard(_ card: CVTemplateCard) {
+        selectedTemplateCardToken = card.selectionToken
+        selectedTemplateID = card.templateID
         applySelectionToCards()
     }
 
-    private func didPreviewTemplate(_ id: String) {
-        selectedTemplateID = id
-        applySelectionToCards()
-        guard let template = activeCatalog.first(where: { $0.id == id }) else { return }
-        presentPlaceholderAlert(
-            title: "Preview \"\(template.name)\"",
-            message: "Full-page previews and PDF export are coming soon."
-        )
-    }
-
     @objc private func didTapUseTemplate() {
         guard let id = selectedTemplateID,
               let template = activeCatalog.first(where: { $0.id == id }) else {
@@ -1173,22 +1176,20 @@ private final class CVChipButton: NSView {
 
 // MARK: - Template card
 
-/// Premium gallery card: live résumé thumbnail, glass-style preview overlay, soft
-/// shadow, and an animated brand border when selected.
+/// Premium gallery card: live résumé thumbnail, soft shadow, and an animated
+/// brand border when selected.
 private final class CVTemplateCard: NSView {
     var onSelect: (() -> Void)?
-    var onPreview: (() -> Void)?
     var isSelected: Bool = false { didSet { applyChrome() } }
+    /// Distinguishes this card from others that may share the same catalog `template.id`.
+    let selectionToken = UUID()
+    var templateID: String { template.id }
     private let template: CVTemplate
     private let palette: CVTemplateCardPalette
     private let previewSurface = NSView()
     private let preview: CVTemplatePreviewView
     private let nameLabel = NSTextField(labelWithString: "")
     private let categoryLabel = NSTextField(labelWithString: "")
-    private let overlay = NSView()
-    private let overlayBadge = NSView()
-    private let overlayBadgeLabel = NSTextField(labelWithString: "Preview")
-    private let overlayBadgeIcon = NSImageView()
     private var trackingArea: NSTrackingArea?
     private var isHovering: Bool = false
     private var didPushCursor: Bool = false
@@ -1248,8 +1249,6 @@ private final class CVTemplateCard: NSView {
 
         addSubview(previewSurface)
         addSubview(footer)
-        addSubview(overlay)
-        configureOverlay()
 
         NSLayoutConstraint.activate([
             previewSurface.topAnchor.constraint(equalTo: topAnchor),
@@ -1265,12 +1264,7 @@ private final class CVTemplateCard: NSView {
             footer.topAnchor.constraint(equalTo: previewSurface.bottomAnchor),
             footer.leadingAnchor.constraint(equalTo: leadingAnchor),
             footer.trailingAnchor.constraint(equalTo: trailingAnchor),
-            footer.bottomAnchor.constraint(equalTo: bottomAnchor),
-
-            overlay.topAnchor.constraint(equalTo: previewSurface.topAnchor),
-            overlay.leadingAnchor.constraint(equalTo: previewSurface.leadingAnchor),
-            overlay.trailingAnchor.constraint(equalTo: previewSurface.trailingAnchor),
-            overlay.bottomAnchor.constraint(equalTo: previewSurface.bottomAnchor)
+            footer.bottomAnchor.constraint(equalTo: bottomAnchor)
         ])
         applyChrome()
     }
@@ -1285,50 +1279,6 @@ private final class CVTemplateCard: NSView {
         layer?.shadowPath = CGPath(roundedRect: bounds, cornerWidth: 24, cornerHeight: 24, transform: nil)
     }
 
-    private func configureOverlay() {
-        overlay.translatesAutoresizingMaskIntoConstraints = false
-        overlay.wantsLayer = true
-        overlay.layer?.backgroundColor = palette.overlayTint.cgColor
-        overlay.alphaValue = 0
-        overlay.isHidden = false
-
-        overlayBadge.translatesAutoresizingMaskIntoConstraints = false
-        overlayBadge.wantsLayer = true
-        overlayBadge.layer?.backgroundColor = NSColor.white.withAlphaComponent(0.22).cgColor
-        overlayBadge.layer?.cornerRadius = 20
-        overlayBadge.layer?.borderWidth = 1
-        overlayBadge.layer?.borderColor = NSColor.white.withAlphaComponent(0.45).cgColor
-
-        overlayBadgeIcon.translatesAutoresizingMaskIntoConstraints = false
-        overlayBadgeIcon.image = NSImage(systemSymbolName: "eye", accessibilityDescription: nil)
-        overlayBadgeIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 12, weight: .semibold)
-        overlayBadgeIcon.contentTintColor = .white
-
-        overlayBadgeLabel.font = .systemFont(ofSize: 12.5, weight: .semibold)
-        overlayBadgeLabel.textColor = .white
-        overlayBadgeLabel.isBordered = false
-        overlayBadgeLabel.drawsBackground = false
-        overlayBadgeLabel.isEditable = false
-        overlayBadgeLabel.isSelectable = false
-
-        let badgeStack = NSStackView(views: [overlayBadgeIcon, overlayBadgeLabel])
-        badgeStack.orientation = .horizontal
-        badgeStack.spacing = 7
-        badgeStack.alignment = .centerY
-        badgeStack.translatesAutoresizingMaskIntoConstraints = false
-
-        overlayBadge.addSubview(badgeStack)
-        overlay.addSubview(overlayBadge)
-        NSLayoutConstraint.activate([
-            badgeStack.leadingAnchor.constraint(equalTo: overlayBadge.leadingAnchor, constant: 16),
-            badgeStack.trailingAnchor.constraint(equalTo: overlayBadge.trailingAnchor, constant: -16),
-            badgeStack.topAnchor.constraint(equalTo: overlayBadge.topAnchor, constant: 9),
-            badgeStack.bottomAnchor.constraint(equalTo: overlayBadge.bottomAnchor, constant: -9),
-            overlayBadge.centerXAnchor.constraint(equalTo: overlay.centerXAnchor),
-            overlayBadge.centerYAnchor.constraint(equalTo: overlay.centerYAnchor)
-        ])
-    }
-
     override func hitTest(_ point: NSPoint) -> NSView? {
         guard let superview else { return super.hitTest(point) }
         let local = convert(point, from: superview)
@@ -1336,13 +1286,8 @@ private final class CVTemplateCard: NSView {
     }
 
     override func mouseDown(with event: NSEvent) {
-        let local = convert(event.locationInWindow, from: nil)
-        if overlay.frame.contains(local) {
-            onPreview?()
-        } else {
-            playTapPulse()
-            onSelect?()
-        }
+        playTapPulse()
+        onSelect?()
     }
 
     private func playTapPulse() {
@@ -1372,7 +1317,6 @@ private final class CVTemplateCard: NSView {
     override func mouseEntered(with event: NSEvent) {
         super.mouseEntered(with: event)
         isHovering = true
-        animateOverlay(visible: true)
         applyChrome()
         if !didPushCursor {
             NSCursor.pointingHand.push()
@@ -1383,7 +1327,6 @@ private final class CVTemplateCard: NSView {
     override func mouseExited(with event: NSEvent) {
         super.mouseExited(with: event)
         isHovering = false
-        animateOverlay(visible: false)
         applyChrome()
         if didPushCursor {
             NSCursor.pop()
@@ -1400,16 +1343,6 @@ private final class CVTemplateCard: NSView {
         }
     }
 
-    private func animateOverlay(visible: Bool) {
-        let target: CGFloat = visible ? 1 : 0
-        NSAnimationContext.runAnimationGroup { context in
-            context.duration = 0.18
-            context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
-            context.allowsImplicitAnimation = true
-            overlay.animator().alphaValue = target
-        }
-    }
-
     private func applyChrome() {
         if isSelected {
             layer?.borderColor = palette.borderSelected.cgColor

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

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