Prechádzať zdrojové kódy

Stabilize CV Maker filter chips and template card layout.

Equal-width filter rows, fixed chip slots, and uniform card chrome prevent layout shifts when switching category groups or selection state.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 2 mesiacov pred
rodič
commit
14152a9801
1 zmenil súbory, kde vykonal 86 pridanie a 39 odobranie
  1. 86 39
      App for Indeed/Views/CVMakerPageView.swift

+ 86 - 39
App for Indeed/Views/CVMakerPageView.swift

@@ -629,6 +629,20 @@ final class CVMakerPageView: NSView {
 
     private var appliedGridColumnCount: Int = 0
 
+    /// Family filter row always renders this many slots so chip widths stay stable
+    /// when switching between Design-Based (3 labels) and Profession-Based (4).
+    private let familyChipSlotCount = 4
+
+    private enum FilterChromeLayout {
+        static let padding: CGFloat = 12
+        static let rowGap: CGFloat = 12
+        static let groupRowHeight: CGFloat = 38
+        static let familyRowHeight: CGFloat = 30
+        static var height: CGFloat {
+            padding * 2 + groupRowHeight + rowGap + familyRowHeight
+        }
+    }
+
     override init(frame frameRect: NSRect) {
         super.init(frame: frameRect)
         configureLayout()
@@ -681,7 +695,12 @@ final class CVMakerPageView: NSView {
             filterStack.leadingAnchor.constraint(equalTo: filterChrome.leadingAnchor, constant: 14),
             filterStack.trailingAnchor.constraint(equalTo: filterChrome.trailingAnchor, constant: -14),
             filterStack.topAnchor.constraint(equalTo: filterChrome.topAnchor, constant: 12),
-            filterStack.bottomAnchor.constraint(equalTo: filterChrome.bottomAnchor, constant: -12)
+            filterStack.bottomAnchor.constraint(equalTo: filterChrome.bottomAnchor, constant: -12),
+            // On this SDK `alignment` is `NSLayoutConstraint.Attribute` (no `.fill`).
+            // Pin row widths so group tabs stay full-width / equal split instead of
+            // shrinking to intrinsic width when selection changes.
+            groupTabsRow.widthAnchor.constraint(equalTo: filterStack.widthAnchor),
+            familyChipsRow.widthAnchor.constraint(equalTo: filterStack.widthAnchor)
         ])
 
         titleLabel.font = .systemFont(ofSize: 22, weight: .bold)
@@ -704,13 +723,16 @@ final class CVMakerPageView: NSView {
         groupTabsRow.alignment = .centerY
         groupTabsRow.distribution = .fillEqually
         groupTabsRow.translatesAutoresizingMaskIntoConstraints = false
+        groupTabsRow.heightAnchor.constraint(equalToConstant: 38).isActive = true
         configureGroupTabs()
 
         familyChipsRow.orientation = .horizontal
         familyChipsRow.spacing = 8
         familyChipsRow.alignment = .centerY
-        familyChipsRow.distribution = .fill
+        // Match the top row: equal-width segments, evenly spaced across the row.
+        familyChipsRow.distribution = .fillEqually
         familyChipsRow.translatesAutoresizingMaskIntoConstraints = false
+        familyChipsRow.heightAnchor.constraint(equalToConstant: 30).isActive = true
 
         gridStack.orientation = .vertical
         gridStack.spacing = 26
@@ -759,8 +781,9 @@ final class CVMakerPageView: NSView {
             headerStack.topAnchor.constraint(equalTo: topAnchor, constant: 8),
 
             filterChrome.leadingAnchor.constraint(equalTo: leadingAnchor, constant: horizontalInset),
-            filterChrome.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor, constant: -horizontalInset),
+            filterChrome.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -horizontalInset),
             filterChrome.topAnchor.constraint(equalTo: headerStack.bottomAnchor, constant: 16),
+            filterChrome.heightAnchor.constraint(equalToConstant: FilterChromeLayout.height),
 
             scrollView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: horizontalInset),
             scrollView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -horizontalInset),
@@ -808,6 +831,7 @@ final class CVMakerPageView: NSView {
 
         let allCount = templates(forGroup: selectedGroup, family: nil).count
         let allChip = CVChipButton(title: "All", badgeText: "\(allCount)", leadingSymbol: nil, style: .pillSmall)
+        allChip.translatesAutoresizingMaskIntoConstraints = false
         allChip.onSelect = { [weak self] in self?.didSelectFamily(nil) }
         familyChipsRow.addArrangedSubview(allChip)
         familyChipButtons[nil] = allChip
@@ -816,16 +840,21 @@ final class CVMakerPageView: NSView {
             let count = templates(forGroup: selectedGroup, family: family).count
             guard count > 0 else { continue }
             let chip = CVChipButton(title: family.title, badgeText: "\(count)", leadingSymbol: nil, style: .pillSmall)
+            chip.translatesAutoresizingMaskIntoConstraints = false
             chip.onSelect = { [weak self] in self?.didSelectFamily(family) }
             familyChipsRow.addArrangedSubview(chip)
             familyChipButtons[family] = chip
         }
 
-        let familyRowTailSpacer = NSView()
-        familyRowTailSpacer.translatesAutoresizingMaskIntoConstraints = false
-        familyRowTailSpacer.setContentHuggingPriority(.init(1), for: .horizontal)
-        familyRowTailSpacer.setContentCompressionResistancePriority(.init(1), for: .horizontal)
-        familyChipsRow.addArrangedSubview(familyRowTailSpacer)
+        // Pad to a fixed slot count so `fillEqually` chip widths never change when
+        // the visible family count differs between category groups.
+        while familyChipsRow.arrangedSubviews.count < familyChipSlotCount {
+            let slot = NSView()
+            slot.translatesAutoresizingMaskIntoConstraints = false
+            slot.setContentHuggingPriority(.defaultLow, for: .horizontal)
+            slot.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
+            familyChipsRow.addArrangedSubview(slot)
+        }
 
         if let f = selectedFamily, templates(forGroup: selectedGroup, family: f).isEmpty {
             selectedFamily = nil
@@ -890,10 +919,12 @@ final class CVMakerPageView: NSView {
                     let filler = NSView()
                     filler.translatesAutoresizingMaskIntoConstraints = false
                     row.addArrangedSubview(filler)
+                    filler.heightAnchor.constraint(equalToConstant: CVTemplateCard.layoutHeight).isActive = true
                 }
             }
             gridStack.addArrangedSubview(row)
             row.widthAnchor.constraint(equalTo: gridStack.widthAnchor).isActive = true
+            row.heightAnchor.constraint(equalToConstant: CVTemplateCard.layoutHeight).isActive = true
             index += columns
         }
 
@@ -1090,11 +1121,12 @@ private final class CVChipButton: NSView {
     private var trackingArea: NSTrackingArea?
 
     private enum Palette {
-        static let restFill = NSColor(srgbRed: 247 / 255, green: 249 / 255, blue: 252 / 255, alpha: 1)
+        static let restFill = NSColor.white
         static let restBorder = NSColor(srgbRed: 222 / 255, green: 226 / 255, blue: 233 / 255, alpha: 1)
-        static let hoverFill = NSColor(srgbRed: 238 / 255, green: 241 / 255, blue: 247 / 255, alpha: 1)
+        static let hoverFill = NSColor(srgbRed: 248 / 255, green: 250 / 255, blue: 252 / 255, alpha: 1)
         static let activeFill = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 1)
         static let activeFillHover = NSColor(srgbRed: 28 / 255, green: 70 / 255, blue: 140 / 255, alpha: 1)
+        static let activeBorder = NSColor(srgbRed: 28 / 255, green: 70 / 255, blue: 140 / 255, alpha: 1)
         static let restText = NSColor(srgbRed: 71 / 255, green: 85 / 255, blue: 105 / 255, alpha: 1)
         static let activeText = NSColor.white
         static let restBadge = NSColor(srgbRed: 230 / 255, green: 234 / 255, blue: 240 / 255, alpha: 1)
@@ -1103,6 +1135,10 @@ private final class CVChipButton: NSView {
         static let activeBadgeText = NSColor.white
     }
 
+    private static let symbolSide: CGFloat = 18
+    private static let badgeWidthLarge: CGFloat = 28
+    private static let badgeWidthSmall: CGFloat = 26
+
     init(title: String, badgeText: String, leadingSymbol: String?, style: Style) {
         self.style = style
         super.init(frame: .zero)
@@ -1116,13 +1152,16 @@ private final class CVChipButton: NSView {
 
         titleLabel.stringValue = title
         titleLabel.font = .systemFont(ofSize: style == .pillLarge ? 13 : 12, weight: .semibold)
+        titleLabel.maximumNumberOfLines = 1
+        titleLabel.lineBreakMode = .byTruncatingTail
+        titleLabel.cell?.lineBreakMode = .byTruncatingTail
         titleLabel.isBordered = false
         titleLabel.drawsBackground = false
         titleLabel.isEditable = false
         titleLabel.isSelectable = false
 
         badgeLabel.stringValue = badgeText
-        badgeLabel.font = .systemFont(ofSize: 10.5, weight: .semibold)
+        badgeLabel.font = .monospacedDigitSystemFont(ofSize: 10.5, weight: .semibold)
         badgeLabel.alignment = .center
         badgeLabel.isBordered = false
         badgeLabel.drawsBackground = false
@@ -1139,7 +1178,9 @@ private final class CVChipButton: NSView {
             badgeLabel.trailingAnchor.constraint(equalTo: badgePill.trailingAnchor, constant: -7),
             badgeLabel.centerYAnchor.constraint(equalTo: badgePill.centerYAnchor),
             badgePill.heightAnchor.constraint(equalToConstant: 18),
-            badgePill.widthAnchor.constraint(greaterThanOrEqualToConstant: 22)
+            badgePill.widthAnchor.constraint(
+                equalToConstant: style == .pillLarge ? Self.badgeWidthLarge : Self.badgeWidthSmall
+            )
         ])
 
         stack.orientation = .horizontal
@@ -1151,30 +1192,38 @@ private final class CVChipButton: NSView {
             symbolView.translatesAutoresizingMaskIntoConstraints = false
             symbolView.image = NSImage(systemSymbolName: symbol, accessibilityDescription: nil)
             symbolView.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 13, weight: .semibold)
+            symbolView.setContentHuggingPriority(.required, for: .horizontal)
+            symbolView.setContentCompressionResistancePriority(.required, for: .horizontal)
             stack.addArrangedSubview(symbolView)
+            NSLayoutConstraint.activate([
+                symbolView.widthAnchor.constraint(equalToConstant: Self.symbolSide),
+                symbolView.heightAnchor.constraint(equalToConstant: Self.symbolSide)
+            ])
         }
         stack.addArrangedSubview(titleLabel)
         stack.addArrangedSubview(badgePill)
 
         addSubview(stack)
         let horizontalInset: CGFloat = style == .pillLarge ? 16 : 12
+        // Leading alignment for every chip so selected vs unselected pills share the
+        // same geometry (centered stacks shift when badge digits change).
         if style == .pillLarge {
-            // Group toggle shares a row: equal widths from the parent stack; keep
-            // icon + label + badge centered so selection state does not reshuffle width.
             NSLayoutConstraint.activate([
-                stack.centerXAnchor.constraint(equalTo: centerXAnchor),
-                stack.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, constant: horizontalInset),
+                stack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: horizontalInset),
                 stack.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor, constant: -horizontalInset),
                 stack.centerYAnchor.constraint(equalTo: centerYAnchor)
             ])
             setContentHuggingPriority(.defaultLow, for: .horizontal)
         } else {
+            // Parent row uses `fillEqually`; center label + badge inside each segment.
             NSLayoutConstraint.activate([
-                stack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: horizontalInset),
-                stack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -horizontalInset),
+                stack.centerXAnchor.constraint(equalTo: centerXAnchor),
+                stack.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, constant: horizontalInset),
+                stack.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor, constant: -horizontalInset),
                 stack.centerYAnchor.constraint(equalTo: centerYAnchor)
             ])
-            setContentHuggingPriority(.required, for: .horizontal)
+            setContentHuggingPriority(.defaultLow, for: .horizontal)
+            setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
         }
         applyState()
     }
@@ -1244,7 +1293,7 @@ private final class CVChipButton: NSView {
         let badgeText: NSColor
         if isSelected {
             fill = isHovering ? Palette.activeFillHover : Palette.activeFill
-            border = fill
+            border = Palette.activeBorder
             textColor = Palette.activeText
             badgeFill = Palette.activeBadge
             badgeText = Palette.activeBadgeText
@@ -1257,6 +1306,7 @@ private final class CVChipButton: NSView {
         }
         layer?.backgroundColor = fill.cgColor
         layer?.borderColor = border.cgColor
+        layer?.borderWidth = 1
         titleLabel.textColor = textColor
         symbolView.contentTintColor = textColor
         badgePill.layer?.backgroundColor = badgeFill.cgColor
@@ -1269,6 +1319,8 @@ private final class CVChipButton: NSView {
 /// Premium gallery card: live résumé thumbnail, soft shadow, and an animated
 /// brand border when selected.
 private final class CVTemplateCard: NSView {
+    static let layoutHeight: CGFloat = 292
+
     var onSelect: (() -> Void)?
     var isSelected: Bool = false { didSet { applyChrome() } }
     /// Distinguishes this card from others that may share the same catalog `template.id`.
@@ -1296,7 +1348,7 @@ private final class CVTemplateCard: NSView {
         layer?.cornerRadius = 24
         layer?.backgroundColor = NSColor.white.cgColor
         translatesAutoresizingMaskIntoConstraints = false
-        heightAnchor.constraint(greaterThanOrEqualToConstant: 292).isActive = true
+        heightAnchor.constraint(equalToConstant: Self.layoutHeight).isActive = true
 
         previewSurface.translatesAutoresizingMaskIntoConstraints = false
         previewSurface.wantsLayer = true
@@ -1436,28 +1488,23 @@ private final class CVTemplateCard: NSView {
     }
 
     private func applyChrome() {
+        // Same border + shadow metrics in every state — only color changes on select
+        // so thumbnails keep identical insets (stroke is drawn inside the layer).
+        let uniformBorder: CGFloat = 2
+        let borderColor: NSColor
         if isSelected {
-            layer?.borderColor = palette.borderSelected.cgColor
-            layer?.borderWidth = 2.5
-            layer?.shadowColor = palette.borderSelected.cgColor
-            layer?.shadowOpacity = 0.42
-            layer?.shadowRadius = 22
-            layer?.shadowOffset = CGSize(width: 0, height: 10)
+            borderColor = palette.borderSelected
         } else if isHovering {
-            layer?.borderColor = palette.borderHover.cgColor
-            layer?.borderWidth = 1.5
-            layer?.shadowColor = NSColor.black.cgColor
-            layer?.shadowOpacity = 0.14
-            layer?.shadowRadius = 18
-            layer?.shadowOffset = CGSize(width: 0, height: 10)
+            borderColor = palette.borderHover
         } else {
-            layer?.borderColor = palette.border.cgColor
-            layer?.borderWidth = 1
-            layer?.shadowColor = NSColor.black.cgColor
-            layer?.shadowOpacity = 0.08
-            layer?.shadowRadius = 12
-            layer?.shadowOffset = CGSize(width: 0, height: 6)
+            borderColor = palette.border
         }
+        layer?.borderColor = borderColor.cgColor
+        layer?.borderWidth = uniformBorder
+        layer?.shadowColor = NSColor.black.cgColor
+        layer?.shadowOpacity = isSelected ? 0.14 : (isHovering ? 0.11 : 0.08)
+        layer?.shadowRadius = 14
+        layer?.shadowOffset = CGSize(width: 0, height: 8)
     }
 }