|
@@ -629,6 +629,20 @@ final class CVMakerPageView: NSView {
|
|
|
|
|
|
|
|
private var appliedGridColumnCount: Int = 0
|
|
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) {
|
|
override init(frame frameRect: NSRect) {
|
|
|
super.init(frame: frameRect)
|
|
super.init(frame: frameRect)
|
|
|
configureLayout()
|
|
configureLayout()
|
|
@@ -681,7 +695,12 @@ final class CVMakerPageView: NSView {
|
|
|
filterStack.leadingAnchor.constraint(equalTo: filterChrome.leadingAnchor, constant: 14),
|
|
filterStack.leadingAnchor.constraint(equalTo: filterChrome.leadingAnchor, constant: 14),
|
|
|
filterStack.trailingAnchor.constraint(equalTo: filterChrome.trailingAnchor, constant: -14),
|
|
filterStack.trailingAnchor.constraint(equalTo: filterChrome.trailingAnchor, constant: -14),
|
|
|
filterStack.topAnchor.constraint(equalTo: filterChrome.topAnchor, constant: 12),
|
|
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)
|
|
titleLabel.font = .systemFont(ofSize: 22, weight: .bold)
|
|
@@ -704,13 +723,16 @@ final class CVMakerPageView: NSView {
|
|
|
groupTabsRow.alignment = .centerY
|
|
groupTabsRow.alignment = .centerY
|
|
|
groupTabsRow.distribution = .fillEqually
|
|
groupTabsRow.distribution = .fillEqually
|
|
|
groupTabsRow.translatesAutoresizingMaskIntoConstraints = false
|
|
groupTabsRow.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ groupTabsRow.heightAnchor.constraint(equalToConstant: 38).isActive = true
|
|
|
configureGroupTabs()
|
|
configureGroupTabs()
|
|
|
|
|
|
|
|
familyChipsRow.orientation = .horizontal
|
|
familyChipsRow.orientation = .horizontal
|
|
|
familyChipsRow.spacing = 8
|
|
familyChipsRow.spacing = 8
|
|
|
familyChipsRow.alignment = .centerY
|
|
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.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ familyChipsRow.heightAnchor.constraint(equalToConstant: 30).isActive = true
|
|
|
|
|
|
|
|
gridStack.orientation = .vertical
|
|
gridStack.orientation = .vertical
|
|
|
gridStack.spacing = 26
|
|
gridStack.spacing = 26
|
|
@@ -759,8 +781,9 @@ final class CVMakerPageView: NSView {
|
|
|
headerStack.topAnchor.constraint(equalTo: topAnchor, constant: 8),
|
|
headerStack.topAnchor.constraint(equalTo: topAnchor, constant: 8),
|
|
|
|
|
|
|
|
filterChrome.leadingAnchor.constraint(equalTo: leadingAnchor, constant: horizontalInset),
|
|
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.topAnchor.constraint(equalTo: headerStack.bottomAnchor, constant: 16),
|
|
|
|
|
+ filterChrome.heightAnchor.constraint(equalToConstant: FilterChromeLayout.height),
|
|
|
|
|
|
|
|
scrollView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: horizontalInset),
|
|
scrollView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: horizontalInset),
|
|
|
scrollView.trailingAnchor.constraint(equalTo: trailingAnchor, 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 allCount = templates(forGroup: selectedGroup, family: nil).count
|
|
|
let allChip = CVChipButton(title: "All", badgeText: "\(allCount)", leadingSymbol: nil, style: .pillSmall)
|
|
let allChip = CVChipButton(title: "All", badgeText: "\(allCount)", leadingSymbol: nil, style: .pillSmall)
|
|
|
|
|
+ allChip.translatesAutoresizingMaskIntoConstraints = false
|
|
|
allChip.onSelect = { [weak self] in self?.didSelectFamily(nil) }
|
|
allChip.onSelect = { [weak self] in self?.didSelectFamily(nil) }
|
|
|
familyChipsRow.addArrangedSubview(allChip)
|
|
familyChipsRow.addArrangedSubview(allChip)
|
|
|
familyChipButtons[nil] = allChip
|
|
familyChipButtons[nil] = allChip
|
|
@@ -816,16 +840,21 @@ final class CVMakerPageView: NSView {
|
|
|
let count = templates(forGroup: selectedGroup, family: family).count
|
|
let count = templates(forGroup: selectedGroup, family: family).count
|
|
|
guard count > 0 else { continue }
|
|
guard count > 0 else { continue }
|
|
|
let chip = CVChipButton(title: family.title, badgeText: "\(count)", leadingSymbol: nil, style: .pillSmall)
|
|
let chip = CVChipButton(title: family.title, badgeText: "\(count)", leadingSymbol: nil, style: .pillSmall)
|
|
|
|
|
+ chip.translatesAutoresizingMaskIntoConstraints = false
|
|
|
chip.onSelect = { [weak self] in self?.didSelectFamily(family) }
|
|
chip.onSelect = { [weak self] in self?.didSelectFamily(family) }
|
|
|
familyChipsRow.addArrangedSubview(chip)
|
|
familyChipsRow.addArrangedSubview(chip)
|
|
|
familyChipButtons[family] = 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 {
|
|
if let f = selectedFamily, templates(forGroup: selectedGroup, family: f).isEmpty {
|
|
|
selectedFamily = nil
|
|
selectedFamily = nil
|
|
@@ -890,10 +919,12 @@ final class CVMakerPageView: NSView {
|
|
|
let filler = NSView()
|
|
let filler = NSView()
|
|
|
filler.translatesAutoresizingMaskIntoConstraints = false
|
|
filler.translatesAutoresizingMaskIntoConstraints = false
|
|
|
row.addArrangedSubview(filler)
|
|
row.addArrangedSubview(filler)
|
|
|
|
|
+ filler.heightAnchor.constraint(equalToConstant: CVTemplateCard.layoutHeight).isActive = true
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
gridStack.addArrangedSubview(row)
|
|
gridStack.addArrangedSubview(row)
|
|
|
row.widthAnchor.constraint(equalTo: gridStack.widthAnchor).isActive = true
|
|
row.widthAnchor.constraint(equalTo: gridStack.widthAnchor).isActive = true
|
|
|
|
|
+ row.heightAnchor.constraint(equalToConstant: CVTemplateCard.layoutHeight).isActive = true
|
|
|
index += columns
|
|
index += columns
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1090,11 +1121,12 @@ private final class CVChipButton: NSView {
|
|
|
private var trackingArea: NSTrackingArea?
|
|
private var trackingArea: NSTrackingArea?
|
|
|
|
|
|
|
|
private enum Palette {
|
|
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 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 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 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 restText = NSColor(srgbRed: 71 / 255, green: 85 / 255, blue: 105 / 255, alpha: 1)
|
|
|
static let activeText = NSColor.white
|
|
static let activeText = NSColor.white
|
|
|
static let restBadge = NSColor(srgbRed: 230 / 255, green: 234 / 255, blue: 240 / 255, alpha: 1)
|
|
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
|
|
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) {
|
|
init(title: String, badgeText: String, leadingSymbol: String?, style: Style) {
|
|
|
self.style = style
|
|
self.style = style
|
|
|
super.init(frame: .zero)
|
|
super.init(frame: .zero)
|
|
@@ -1116,13 +1152,16 @@ private final class CVChipButton: NSView {
|
|
|
|
|
|
|
|
titleLabel.stringValue = title
|
|
titleLabel.stringValue = title
|
|
|
titleLabel.font = .systemFont(ofSize: style == .pillLarge ? 13 : 12, weight: .semibold)
|
|
titleLabel.font = .systemFont(ofSize: style == .pillLarge ? 13 : 12, weight: .semibold)
|
|
|
|
|
+ titleLabel.maximumNumberOfLines = 1
|
|
|
|
|
+ titleLabel.lineBreakMode = .byTruncatingTail
|
|
|
|
|
+ titleLabel.cell?.lineBreakMode = .byTruncatingTail
|
|
|
titleLabel.isBordered = false
|
|
titleLabel.isBordered = false
|
|
|
titleLabel.drawsBackground = false
|
|
titleLabel.drawsBackground = false
|
|
|
titleLabel.isEditable = false
|
|
titleLabel.isEditable = false
|
|
|
titleLabel.isSelectable = false
|
|
titleLabel.isSelectable = false
|
|
|
|
|
|
|
|
badgeLabel.stringValue = badgeText
|
|
badgeLabel.stringValue = badgeText
|
|
|
- badgeLabel.font = .systemFont(ofSize: 10.5, weight: .semibold)
|
|
|
|
|
|
|
+ badgeLabel.font = .monospacedDigitSystemFont(ofSize: 10.5, weight: .semibold)
|
|
|
badgeLabel.alignment = .center
|
|
badgeLabel.alignment = .center
|
|
|
badgeLabel.isBordered = false
|
|
badgeLabel.isBordered = false
|
|
|
badgeLabel.drawsBackground = false
|
|
badgeLabel.drawsBackground = false
|
|
@@ -1139,7 +1178,9 @@ private final class CVChipButton: NSView {
|
|
|
badgeLabel.trailingAnchor.constraint(equalTo: badgePill.trailingAnchor, constant: -7),
|
|
badgeLabel.trailingAnchor.constraint(equalTo: badgePill.trailingAnchor, constant: -7),
|
|
|
badgeLabel.centerYAnchor.constraint(equalTo: badgePill.centerYAnchor),
|
|
badgeLabel.centerYAnchor.constraint(equalTo: badgePill.centerYAnchor),
|
|
|
badgePill.heightAnchor.constraint(equalToConstant: 18),
|
|
badgePill.heightAnchor.constraint(equalToConstant: 18),
|
|
|
- badgePill.widthAnchor.constraint(greaterThanOrEqualToConstant: 22)
|
|
|
|
|
|
|
+ badgePill.widthAnchor.constraint(
|
|
|
|
|
+ equalToConstant: style == .pillLarge ? Self.badgeWidthLarge : Self.badgeWidthSmall
|
|
|
|
|
+ )
|
|
|
])
|
|
])
|
|
|
|
|
|
|
|
stack.orientation = .horizontal
|
|
stack.orientation = .horizontal
|
|
@@ -1151,30 +1192,38 @@ private final class CVChipButton: NSView {
|
|
|
symbolView.translatesAutoresizingMaskIntoConstraints = false
|
|
symbolView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
symbolView.image = NSImage(systemSymbolName: symbol, accessibilityDescription: nil)
|
|
symbolView.image = NSImage(systemSymbolName: symbol, accessibilityDescription: nil)
|
|
|
symbolView.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 13, weight: .semibold)
|
|
symbolView.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 13, weight: .semibold)
|
|
|
|
|
+ symbolView.setContentHuggingPriority(.required, for: .horizontal)
|
|
|
|
|
+ symbolView.setContentCompressionResistancePriority(.required, for: .horizontal)
|
|
|
stack.addArrangedSubview(symbolView)
|
|
stack.addArrangedSubview(symbolView)
|
|
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
|
|
+ symbolView.widthAnchor.constraint(equalToConstant: Self.symbolSide),
|
|
|
|
|
+ symbolView.heightAnchor.constraint(equalToConstant: Self.symbolSide)
|
|
|
|
|
+ ])
|
|
|
}
|
|
}
|
|
|
stack.addArrangedSubview(titleLabel)
|
|
stack.addArrangedSubview(titleLabel)
|
|
|
stack.addArrangedSubview(badgePill)
|
|
stack.addArrangedSubview(badgePill)
|
|
|
|
|
|
|
|
addSubview(stack)
|
|
addSubview(stack)
|
|
|
let horizontalInset: CGFloat = style == .pillLarge ? 16 : 12
|
|
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 {
|
|
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([
|
|
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.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor, constant: -horizontalInset),
|
|
|
stack.centerYAnchor.constraint(equalTo: centerYAnchor)
|
|
stack.centerYAnchor.constraint(equalTo: centerYAnchor)
|
|
|
])
|
|
])
|
|
|
setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
} else {
|
|
} else {
|
|
|
|
|
+ // Parent row uses `fillEqually`; center label + badge inside each segment.
|
|
|
NSLayoutConstraint.activate([
|
|
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)
|
|
stack.centerYAnchor.constraint(equalTo: centerYAnchor)
|
|
|
])
|
|
])
|
|
|
- setContentHuggingPriority(.required, for: .horizontal)
|
|
|
|
|
|
|
+ setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
|
|
+ setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
|
}
|
|
}
|
|
|
applyState()
|
|
applyState()
|
|
|
}
|
|
}
|
|
@@ -1244,7 +1293,7 @@ private final class CVChipButton: NSView {
|
|
|
let badgeText: NSColor
|
|
let badgeText: NSColor
|
|
|
if isSelected {
|
|
if isSelected {
|
|
|
fill = isHovering ? Palette.activeFillHover : Palette.activeFill
|
|
fill = isHovering ? Palette.activeFillHover : Palette.activeFill
|
|
|
- border = fill
|
|
|
|
|
|
|
+ border = Palette.activeBorder
|
|
|
textColor = Palette.activeText
|
|
textColor = Palette.activeText
|
|
|
badgeFill = Palette.activeBadge
|
|
badgeFill = Palette.activeBadge
|
|
|
badgeText = Palette.activeBadgeText
|
|
badgeText = Palette.activeBadgeText
|
|
@@ -1257,6 +1306,7 @@ private final class CVChipButton: NSView {
|
|
|
}
|
|
}
|
|
|
layer?.backgroundColor = fill.cgColor
|
|
layer?.backgroundColor = fill.cgColor
|
|
|
layer?.borderColor = border.cgColor
|
|
layer?.borderColor = border.cgColor
|
|
|
|
|
+ layer?.borderWidth = 1
|
|
|
titleLabel.textColor = textColor
|
|
titleLabel.textColor = textColor
|
|
|
symbolView.contentTintColor = textColor
|
|
symbolView.contentTintColor = textColor
|
|
|
badgePill.layer?.backgroundColor = badgeFill.cgColor
|
|
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
|
|
/// Premium gallery card: live résumé thumbnail, soft shadow, and an animated
|
|
|
/// brand border when selected.
|
|
/// brand border when selected.
|
|
|
private final class CVTemplateCard: NSView {
|
|
private final class CVTemplateCard: NSView {
|
|
|
|
|
+ static let layoutHeight: CGFloat = 292
|
|
|
|
|
+
|
|
|
var onSelect: (() -> Void)?
|
|
var onSelect: (() -> Void)?
|
|
|
var isSelected: Bool = false { didSet { applyChrome() } }
|
|
var isSelected: Bool = false { didSet { applyChrome() } }
|
|
|
/// Distinguishes this card from others that may share the same catalog `template.id`.
|
|
/// 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?.cornerRadius = 24
|
|
|
layer?.backgroundColor = NSColor.white.cgColor
|
|
layer?.backgroundColor = NSColor.white.cgColor
|
|
|
translatesAutoresizingMaskIntoConstraints = false
|
|
translatesAutoresizingMaskIntoConstraints = false
|
|
|
- heightAnchor.constraint(greaterThanOrEqualToConstant: 292).isActive = true
|
|
|
|
|
|
|
+ heightAnchor.constraint(equalToConstant: Self.layoutHeight).isActive = true
|
|
|
|
|
|
|
|
previewSurface.translatesAutoresizingMaskIntoConstraints = false
|
|
previewSurface.translatesAutoresizingMaskIntoConstraints = false
|
|
|
previewSurface.wantsLayer = true
|
|
previewSurface.wantsLayer = true
|
|
@@ -1436,28 +1488,23 @@ private final class CVTemplateCard: NSView {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private func applyChrome() {
|
|
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 {
|
|
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 {
|
|
} 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 {
|
|
} 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)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|