|
|
@@ -629,6 +629,20 @@ final class CVMakerPageView: NSView {
|
|
629
|
629
|
|
|
630
|
630
|
private var appliedGridColumnCount: Int = 0
|
|
631
|
631
|
|
|
|
632
|
+ /// Family filter row always renders this many slots so chip widths stay stable
|
|
|
633
|
+ /// when switching between Design-Based (3 labels) and Profession-Based (4).
|
|
|
634
|
+ private let familyChipSlotCount = 4
|
|
|
635
|
+
|
|
|
636
|
+ private enum FilterChromeLayout {
|
|
|
637
|
+ static let padding: CGFloat = 12
|
|
|
638
|
+ static let rowGap: CGFloat = 12
|
|
|
639
|
+ static let groupRowHeight: CGFloat = 38
|
|
|
640
|
+ static let familyRowHeight: CGFloat = 30
|
|
|
641
|
+ static var height: CGFloat {
|
|
|
642
|
+ padding * 2 + groupRowHeight + rowGap + familyRowHeight
|
|
|
643
|
+ }
|
|
|
644
|
+ }
|
|
|
645
|
+
|
|
632
|
646
|
override init(frame frameRect: NSRect) {
|
|
633
|
647
|
super.init(frame: frameRect)
|
|
634
|
648
|
configureLayout()
|
|
|
@@ -681,7 +695,12 @@ final class CVMakerPageView: NSView {
|
|
681
|
695
|
filterStack.leadingAnchor.constraint(equalTo: filterChrome.leadingAnchor, constant: 14),
|
|
682
|
696
|
filterStack.trailingAnchor.constraint(equalTo: filterChrome.trailingAnchor, constant: -14),
|
|
683
|
697
|
filterStack.topAnchor.constraint(equalTo: filterChrome.topAnchor, constant: 12),
|
|
684
|
|
- filterStack.bottomAnchor.constraint(equalTo: filterChrome.bottomAnchor, constant: -12)
|
|
|
698
|
+ filterStack.bottomAnchor.constraint(equalTo: filterChrome.bottomAnchor, constant: -12),
|
|
|
699
|
+ // On this SDK `alignment` is `NSLayoutConstraint.Attribute` (no `.fill`).
|
|
|
700
|
+ // Pin row widths so group tabs stay full-width / equal split instead of
|
|
|
701
|
+ // shrinking to intrinsic width when selection changes.
|
|
|
702
|
+ groupTabsRow.widthAnchor.constraint(equalTo: filterStack.widthAnchor),
|
|
|
703
|
+ familyChipsRow.widthAnchor.constraint(equalTo: filterStack.widthAnchor)
|
|
685
|
704
|
])
|
|
686
|
705
|
|
|
687
|
706
|
titleLabel.font = .systemFont(ofSize: 22, weight: .bold)
|
|
|
@@ -704,13 +723,16 @@ final class CVMakerPageView: NSView {
|
|
704
|
723
|
groupTabsRow.alignment = .centerY
|
|
705
|
724
|
groupTabsRow.distribution = .fillEqually
|
|
706
|
725
|
groupTabsRow.translatesAutoresizingMaskIntoConstraints = false
|
|
|
726
|
+ groupTabsRow.heightAnchor.constraint(equalToConstant: 38).isActive = true
|
|
707
|
727
|
configureGroupTabs()
|
|
708
|
728
|
|
|
709
|
729
|
familyChipsRow.orientation = .horizontal
|
|
710
|
730
|
familyChipsRow.spacing = 8
|
|
711
|
731
|
familyChipsRow.alignment = .centerY
|
|
712
|
|
- familyChipsRow.distribution = .fill
|
|
|
732
|
+ // Match the top row: equal-width segments, evenly spaced across the row.
|
|
|
733
|
+ familyChipsRow.distribution = .fillEqually
|
|
713
|
734
|
familyChipsRow.translatesAutoresizingMaskIntoConstraints = false
|
|
|
735
|
+ familyChipsRow.heightAnchor.constraint(equalToConstant: 30).isActive = true
|
|
714
|
736
|
|
|
715
|
737
|
gridStack.orientation = .vertical
|
|
716
|
738
|
gridStack.spacing = 26
|
|
|
@@ -759,8 +781,9 @@ final class CVMakerPageView: NSView {
|
|
759
|
781
|
headerStack.topAnchor.constraint(equalTo: topAnchor, constant: 8),
|
|
760
|
782
|
|
|
761
|
783
|
filterChrome.leadingAnchor.constraint(equalTo: leadingAnchor, constant: horizontalInset),
|
|
762
|
|
- filterChrome.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor, constant: -horizontalInset),
|
|
|
784
|
+ filterChrome.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -horizontalInset),
|
|
763
|
785
|
filterChrome.topAnchor.constraint(equalTo: headerStack.bottomAnchor, constant: 16),
|
|
|
786
|
+ filterChrome.heightAnchor.constraint(equalToConstant: FilterChromeLayout.height),
|
|
764
|
787
|
|
|
765
|
788
|
scrollView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: horizontalInset),
|
|
766
|
789
|
scrollView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -horizontalInset),
|
|
|
@@ -808,6 +831,7 @@ final class CVMakerPageView: NSView {
|
|
808
|
831
|
|
|
809
|
832
|
let allCount = templates(forGroup: selectedGroup, family: nil).count
|
|
810
|
833
|
let allChip = CVChipButton(title: "All", badgeText: "\(allCount)", leadingSymbol: nil, style: .pillSmall)
|
|
|
834
|
+ allChip.translatesAutoresizingMaskIntoConstraints = false
|
|
811
|
835
|
allChip.onSelect = { [weak self] in self?.didSelectFamily(nil) }
|
|
812
|
836
|
familyChipsRow.addArrangedSubview(allChip)
|
|
813
|
837
|
familyChipButtons[nil] = allChip
|
|
|
@@ -816,16 +840,21 @@ final class CVMakerPageView: NSView {
|
|
816
|
840
|
let count = templates(forGroup: selectedGroup, family: family).count
|
|
817
|
841
|
guard count > 0 else { continue }
|
|
818
|
842
|
let chip = CVChipButton(title: family.title, badgeText: "\(count)", leadingSymbol: nil, style: .pillSmall)
|
|
|
843
|
+ chip.translatesAutoresizingMaskIntoConstraints = false
|
|
819
|
844
|
chip.onSelect = { [weak self] in self?.didSelectFamily(family) }
|
|
820
|
845
|
familyChipsRow.addArrangedSubview(chip)
|
|
821
|
846
|
familyChipButtons[family] = chip
|
|
822
|
847
|
}
|
|
823
|
848
|
|
|
824
|
|
- let familyRowTailSpacer = NSView()
|
|
825
|
|
- familyRowTailSpacer.translatesAutoresizingMaskIntoConstraints = false
|
|
826
|
|
- familyRowTailSpacer.setContentHuggingPriority(.init(1), for: .horizontal)
|
|
827
|
|
- familyRowTailSpacer.setContentCompressionResistancePriority(.init(1), for: .horizontal)
|
|
828
|
|
- familyChipsRow.addArrangedSubview(familyRowTailSpacer)
|
|
|
849
|
+ // Pad to a fixed slot count so `fillEqually` chip widths never change when
|
|
|
850
|
+ // the visible family count differs between category groups.
|
|
|
851
|
+ while familyChipsRow.arrangedSubviews.count < familyChipSlotCount {
|
|
|
852
|
+ let slot = NSView()
|
|
|
853
|
+ slot.translatesAutoresizingMaskIntoConstraints = false
|
|
|
854
|
+ slot.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
855
|
+ slot.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
|
856
|
+ familyChipsRow.addArrangedSubview(slot)
|
|
|
857
|
+ }
|
|
829
|
858
|
|
|
830
|
859
|
if let f = selectedFamily, templates(forGroup: selectedGroup, family: f).isEmpty {
|
|
831
|
860
|
selectedFamily = nil
|
|
|
@@ -890,10 +919,12 @@ final class CVMakerPageView: NSView {
|
|
890
|
919
|
let filler = NSView()
|
|
891
|
920
|
filler.translatesAutoresizingMaskIntoConstraints = false
|
|
892
|
921
|
row.addArrangedSubview(filler)
|
|
|
922
|
+ filler.heightAnchor.constraint(equalToConstant: CVTemplateCard.layoutHeight).isActive = true
|
|
893
|
923
|
}
|
|
894
|
924
|
}
|
|
895
|
925
|
gridStack.addArrangedSubview(row)
|
|
896
|
926
|
row.widthAnchor.constraint(equalTo: gridStack.widthAnchor).isActive = true
|
|
|
927
|
+ row.heightAnchor.constraint(equalToConstant: CVTemplateCard.layoutHeight).isActive = true
|
|
897
|
928
|
index += columns
|
|
898
|
929
|
}
|
|
899
|
930
|
|
|
|
@@ -1090,11 +1121,12 @@ private final class CVChipButton: NSView {
|
|
1090
|
1121
|
private var trackingArea: NSTrackingArea?
|
|
1091
|
1122
|
|
|
1092
|
1123
|
private enum Palette {
|
|
1093
|
|
- static let restFill = NSColor(srgbRed: 247 / 255, green: 249 / 255, blue: 252 / 255, alpha: 1)
|
|
|
1124
|
+ static let restFill = NSColor.white
|
|
1094
|
1125
|
static let restBorder = NSColor(srgbRed: 222 / 255, green: 226 / 255, blue: 233 / 255, alpha: 1)
|
|
1095
|
|
- static let hoverFill = NSColor(srgbRed: 238 / 255, green: 241 / 255, blue: 247 / 255, alpha: 1)
|
|
|
1126
|
+ static let hoverFill = NSColor(srgbRed: 248 / 255, green: 250 / 255, blue: 252 / 255, alpha: 1)
|
|
1096
|
1127
|
static let activeFill = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 1)
|
|
1097
|
1128
|
static let activeFillHover = NSColor(srgbRed: 28 / 255, green: 70 / 255, blue: 140 / 255, alpha: 1)
|
|
|
1129
|
+ static let activeBorder = NSColor(srgbRed: 28 / 255, green: 70 / 255, blue: 140 / 255, alpha: 1)
|
|
1098
|
1130
|
static let restText = NSColor(srgbRed: 71 / 255, green: 85 / 255, blue: 105 / 255, alpha: 1)
|
|
1099
|
1131
|
static let activeText = NSColor.white
|
|
1100
|
1132
|
static let restBadge = NSColor(srgbRed: 230 / 255, green: 234 / 255, blue: 240 / 255, alpha: 1)
|
|
|
@@ -1103,6 +1135,10 @@ private final class CVChipButton: NSView {
|
|
1103
|
1135
|
static let activeBadgeText = NSColor.white
|
|
1104
|
1136
|
}
|
|
1105
|
1137
|
|
|
|
1138
|
+ private static let symbolSide: CGFloat = 18
|
|
|
1139
|
+ private static let badgeWidthLarge: CGFloat = 28
|
|
|
1140
|
+ private static let badgeWidthSmall: CGFloat = 26
|
|
|
1141
|
+
|
|
1106
|
1142
|
init(title: String, badgeText: String, leadingSymbol: String?, style: Style) {
|
|
1107
|
1143
|
self.style = style
|
|
1108
|
1144
|
super.init(frame: .zero)
|
|
|
@@ -1116,13 +1152,16 @@ private final class CVChipButton: NSView {
|
|
1116
|
1152
|
|
|
1117
|
1153
|
titleLabel.stringValue = title
|
|
1118
|
1154
|
titleLabel.font = .systemFont(ofSize: style == .pillLarge ? 13 : 12, weight: .semibold)
|
|
|
1155
|
+ titleLabel.maximumNumberOfLines = 1
|
|
|
1156
|
+ titleLabel.lineBreakMode = .byTruncatingTail
|
|
|
1157
|
+ titleLabel.cell?.lineBreakMode = .byTruncatingTail
|
|
1119
|
1158
|
titleLabel.isBordered = false
|
|
1120
|
1159
|
titleLabel.drawsBackground = false
|
|
1121
|
1160
|
titleLabel.isEditable = false
|
|
1122
|
1161
|
titleLabel.isSelectable = false
|
|
1123
|
1162
|
|
|
1124
|
1163
|
badgeLabel.stringValue = badgeText
|
|
1125
|
|
- badgeLabel.font = .systemFont(ofSize: 10.5, weight: .semibold)
|
|
|
1164
|
+ badgeLabel.font = .monospacedDigitSystemFont(ofSize: 10.5, weight: .semibold)
|
|
1126
|
1165
|
badgeLabel.alignment = .center
|
|
1127
|
1166
|
badgeLabel.isBordered = false
|
|
1128
|
1167
|
badgeLabel.drawsBackground = false
|
|
|
@@ -1139,7 +1178,9 @@ private final class CVChipButton: NSView {
|
|
1139
|
1178
|
badgeLabel.trailingAnchor.constraint(equalTo: badgePill.trailingAnchor, constant: -7),
|
|
1140
|
1179
|
badgeLabel.centerYAnchor.constraint(equalTo: badgePill.centerYAnchor),
|
|
1141
|
1180
|
badgePill.heightAnchor.constraint(equalToConstant: 18),
|
|
1142
|
|
- badgePill.widthAnchor.constraint(greaterThanOrEqualToConstant: 22)
|
|
|
1181
|
+ badgePill.widthAnchor.constraint(
|
|
|
1182
|
+ equalToConstant: style == .pillLarge ? Self.badgeWidthLarge : Self.badgeWidthSmall
|
|
|
1183
|
+ )
|
|
1143
|
1184
|
])
|
|
1144
|
1185
|
|
|
1145
|
1186
|
stack.orientation = .horizontal
|
|
|
@@ -1151,30 +1192,38 @@ private final class CVChipButton: NSView {
|
|
1151
|
1192
|
symbolView.translatesAutoresizingMaskIntoConstraints = false
|
|
1152
|
1193
|
symbolView.image = NSImage(systemSymbolName: symbol, accessibilityDescription: nil)
|
|
1153
|
1194
|
symbolView.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 13, weight: .semibold)
|
|
|
1195
|
+ symbolView.setContentHuggingPriority(.required, for: .horizontal)
|
|
|
1196
|
+ symbolView.setContentCompressionResistancePriority(.required, for: .horizontal)
|
|
1154
|
1197
|
stack.addArrangedSubview(symbolView)
|
|
|
1198
|
+ NSLayoutConstraint.activate([
|
|
|
1199
|
+ symbolView.widthAnchor.constraint(equalToConstant: Self.symbolSide),
|
|
|
1200
|
+ symbolView.heightAnchor.constraint(equalToConstant: Self.symbolSide)
|
|
|
1201
|
+ ])
|
|
1155
|
1202
|
}
|
|
1156
|
1203
|
stack.addArrangedSubview(titleLabel)
|
|
1157
|
1204
|
stack.addArrangedSubview(badgePill)
|
|
1158
|
1205
|
|
|
1159
|
1206
|
addSubview(stack)
|
|
1160
|
1207
|
let horizontalInset: CGFloat = style == .pillLarge ? 16 : 12
|
|
|
1208
|
+ // Leading alignment for every chip so selected vs unselected pills share the
|
|
|
1209
|
+ // same geometry (centered stacks shift when badge digits change).
|
|
1161
|
1210
|
if style == .pillLarge {
|
|
1162
|
|
- // Group toggle shares a row: equal widths from the parent stack; keep
|
|
1163
|
|
- // icon + label + badge centered so selection state does not reshuffle width.
|
|
1164
|
1211
|
NSLayoutConstraint.activate([
|
|
1165
|
|
- stack.centerXAnchor.constraint(equalTo: centerXAnchor),
|
|
1166
|
|
- stack.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, constant: horizontalInset),
|
|
|
1212
|
+ stack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: horizontalInset),
|
|
1167
|
1213
|
stack.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor, constant: -horizontalInset),
|
|
1168
|
1214
|
stack.centerYAnchor.constraint(equalTo: centerYAnchor)
|
|
1169
|
1215
|
])
|
|
1170
|
1216
|
setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
1171
|
1217
|
} else {
|
|
|
1218
|
+ // Parent row uses `fillEqually`; center label + badge inside each segment.
|
|
1172
|
1219
|
NSLayoutConstraint.activate([
|
|
1173
|
|
- stack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: horizontalInset),
|
|
1174
|
|
- stack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -horizontalInset),
|
|
|
1220
|
+ stack.centerXAnchor.constraint(equalTo: centerXAnchor),
|
|
|
1221
|
+ stack.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, constant: horizontalInset),
|
|
|
1222
|
+ stack.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor, constant: -horizontalInset),
|
|
1175
|
1223
|
stack.centerYAnchor.constraint(equalTo: centerYAnchor)
|
|
1176
|
1224
|
])
|
|
1177
|
|
- setContentHuggingPriority(.required, for: .horizontal)
|
|
|
1225
|
+ setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
1226
|
+ setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
1178
|
1227
|
}
|
|
1179
|
1228
|
applyState()
|
|
1180
|
1229
|
}
|
|
|
@@ -1244,7 +1293,7 @@ private final class CVChipButton: NSView {
|
|
1244
|
1293
|
let badgeText: NSColor
|
|
1245
|
1294
|
if isSelected {
|
|
1246
|
1295
|
fill = isHovering ? Palette.activeFillHover : Palette.activeFill
|
|
1247
|
|
- border = fill
|
|
|
1296
|
+ border = Palette.activeBorder
|
|
1248
|
1297
|
textColor = Palette.activeText
|
|
1249
|
1298
|
badgeFill = Palette.activeBadge
|
|
1250
|
1299
|
badgeText = Palette.activeBadgeText
|
|
|
@@ -1257,6 +1306,7 @@ private final class CVChipButton: NSView {
|
|
1257
|
1306
|
}
|
|
1258
|
1307
|
layer?.backgroundColor = fill.cgColor
|
|
1259
|
1308
|
layer?.borderColor = border.cgColor
|
|
|
1309
|
+ layer?.borderWidth = 1
|
|
1260
|
1310
|
titleLabel.textColor = textColor
|
|
1261
|
1311
|
symbolView.contentTintColor = textColor
|
|
1262
|
1312
|
badgePill.layer?.backgroundColor = badgeFill.cgColor
|
|
|
@@ -1269,6 +1319,8 @@ private final class CVChipButton: NSView {
|
|
1269
|
1319
|
/// Premium gallery card: live résumé thumbnail, soft shadow, and an animated
|
|
1270
|
1320
|
/// brand border when selected.
|
|
1271
|
1321
|
private final class CVTemplateCard: NSView {
|
|
|
1322
|
+ static let layoutHeight: CGFloat = 292
|
|
|
1323
|
+
|
|
1272
|
1324
|
var onSelect: (() -> Void)?
|
|
1273
|
1325
|
var isSelected: Bool = false { didSet { applyChrome() } }
|
|
1274
|
1326
|
/// Distinguishes this card from others that may share the same catalog `template.id`.
|
|
|
@@ -1296,7 +1348,7 @@ private final class CVTemplateCard: NSView {
|
|
1296
|
1348
|
layer?.cornerRadius = 24
|
|
1297
|
1349
|
layer?.backgroundColor = NSColor.white.cgColor
|
|
1298
|
1350
|
translatesAutoresizingMaskIntoConstraints = false
|
|
1299
|
|
- heightAnchor.constraint(greaterThanOrEqualToConstant: 292).isActive = true
|
|
|
1351
|
+ heightAnchor.constraint(equalToConstant: Self.layoutHeight).isActive = true
|
|
1300
|
1352
|
|
|
1301
|
1353
|
previewSurface.translatesAutoresizingMaskIntoConstraints = false
|
|
1302
|
1354
|
previewSurface.wantsLayer = true
|
|
|
@@ -1436,28 +1488,23 @@ private final class CVTemplateCard: NSView {
|
|
1436
|
1488
|
}
|
|
1437
|
1489
|
|
|
1438
|
1490
|
private func applyChrome() {
|
|
|
1491
|
+ // Same border + shadow metrics in every state — only color changes on select
|
|
|
1492
|
+ // so thumbnails keep identical insets (stroke is drawn inside the layer).
|
|
|
1493
|
+ let uniformBorder: CGFloat = 2
|
|
|
1494
|
+ let borderColor: NSColor
|
|
1439
|
1495
|
if isSelected {
|
|
1440
|
|
- layer?.borderColor = palette.borderSelected.cgColor
|
|
1441
|
|
- layer?.borderWidth = 2.5
|
|
1442
|
|
- layer?.shadowColor = palette.borderSelected.cgColor
|
|
1443
|
|
- layer?.shadowOpacity = 0.42
|
|
1444
|
|
- layer?.shadowRadius = 22
|
|
1445
|
|
- layer?.shadowOffset = CGSize(width: 0, height: 10)
|
|
|
1496
|
+ borderColor = palette.borderSelected
|
|
1446
|
1497
|
} else if isHovering {
|
|
1447
|
|
- layer?.borderColor = palette.borderHover.cgColor
|
|
1448
|
|
- layer?.borderWidth = 1.5
|
|
1449
|
|
- layer?.shadowColor = NSColor.black.cgColor
|
|
1450
|
|
- layer?.shadowOpacity = 0.14
|
|
1451
|
|
- layer?.shadowRadius = 18
|
|
1452
|
|
- layer?.shadowOffset = CGSize(width: 0, height: 10)
|
|
|
1498
|
+ borderColor = palette.borderHover
|
|
1453
|
1499
|
} else {
|
|
1454
|
|
- layer?.borderColor = palette.border.cgColor
|
|
1455
|
|
- layer?.borderWidth = 1
|
|
1456
|
|
- layer?.shadowColor = NSColor.black.cgColor
|
|
1457
|
|
- layer?.shadowOpacity = 0.08
|
|
1458
|
|
- layer?.shadowRadius = 12
|
|
1459
|
|
- layer?.shadowOffset = CGSize(width: 0, height: 6)
|
|
|
1500
|
+ borderColor = palette.border
|
|
1460
|
1501
|
}
|
|
|
1502
|
+ layer?.borderColor = borderColor.cgColor
|
|
|
1503
|
+ layer?.borderWidth = uniformBorder
|
|
|
1504
|
+ layer?.shadowColor = NSColor.black.cgColor
|
|
|
1505
|
+ layer?.shadowOpacity = isSelected ? 0.14 : (isHovering ? 0.11 : 0.08)
|
|
|
1506
|
+ layer?.shadowRadius = 14
|
|
|
1507
|
+ layer?.shadowOffset = CGSize(width: 0, height: 8)
|
|
1461
|
1508
|
}
|
|
1462
|
1509
|
}
|
|
1463
|
1510
|
|