暂无描述

CVTemplateMiniPreview.swift 50KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042
  1. //
  2. // CVTemplateMiniPreview.swift
  3. // App for Indeed
  4. //
  5. // Realistic mini résumé thumbnails for the template gallery — typography,
  6. // spacing, and layout vary by design family so cards read as finished previews.
  7. //
  8. import Cocoa
  9. // MARK: - Palette (shared with gallery card)
  10. struct CVTemplateCardPalette {
  11. let border: NSColor
  12. let borderHover: NSColor
  13. let borderSelected: NSColor
  14. let selectionGlow: NSColor
  15. let footerBackground: NSColor
  16. let previewSurface: NSColor
  17. let previewPaper: NSColor
  18. let previewSidebarTint: NSColor
  19. let previewInk: NSColor
  20. let previewMuted: NSColor
  21. let previewAccentRed: NSColor
  22. let previewAccentBlue: NSColor
  23. let primaryText: NSColor
  24. let secondaryText: NSColor
  25. let overlayTint: NSColor
  26. }
  27. // MARK: - Demo résumé content
  28. fileprivate enum CVPreviewDemoContent {
  29. static let fullName = "Sarah Johnson"
  30. static let title = "Senior Product Manager"
  31. static let company = "Google"
  32. static let companyLine = "Google · Mountain View, CA · 2019 – Present"
  33. static let university = "Stanford University"
  34. static let degree = "M.S. Management Science & Engineering"
  35. static let email = "sarah.johnson@email.com"
  36. static let phone = "(415) 555-0198"
  37. static let location = "Mountain View, CA"
  38. static let summary = "Product leader shipping roadmap, discovery, and analytics for high-scale consumer experiences."
  39. static let bullet1 = "Defined multi-year platform strategy with exec stakeholders and quarterly OKRs."
  40. static let bullet2 = "Partnered with engineering and design to launch experiments improving activation by 12%."
  41. static let skillsList = ["Product Strategy", "SQL", "Figma", "A/B Testing", "Roadmapping"]
  42. }
  43. // MARK: - Mini preview
  44. final class CVTemplatePreviewView: NSView {
  45. private let template: CVTemplate
  46. private let palette: CVTemplateCardPalette
  47. private let paper = NSView()
  48. /// Stable 0…11 from template identity so each row looks different (not just by `family`).
  49. private var idVariant: Int {
  50. var h: UInt64 = 1469598103934665603
  51. let layoutDesc: String
  52. switch template.layout {
  53. case .singleColumn: layoutDesc = "1col"
  54. case .twoColumn(let s, let t): layoutDesc = "2col_\(s)_\(t)"
  55. }
  56. let blob = "\(template.id)|\(template.family.rawValue)|\(template.headline)|\(template.accent)|\(layoutDesc)|\(template.sectionLabelStyle)"
  57. for b in blob.utf8 {
  58. h ^= UInt64(b)
  59. h &*= 1_099_511_628_211
  60. }
  61. return Int(h % 12)
  62. }
  63. init(template: CVTemplate, palette: CVTemplateCardPalette) {
  64. self.template = template
  65. self.palette = palette
  66. super.init(frame: .zero)
  67. wantsLayer = true
  68. translatesAutoresizingMaskIntoConstraints = false
  69. configurePaper()
  70. }
  71. @available(*, unavailable)
  72. required init?(coder: NSCoder) {
  73. fatalError("init(coder:) has not been implemented")
  74. }
  75. private func configurePaper() {
  76. paper.translatesAutoresizingMaskIntoConstraints = false
  77. paper.wantsLayer = true
  78. paper.layer?.backgroundColor = paperBackgroundColor().cgColor
  79. paper.layer?.cornerRadius = 6
  80. paper.layer?.borderColor = NSColor(srgbRed: 228 / 255, green: 232 / 255, blue: 240 / 255, alpha: 1).cgColor
  81. paper.layer?.borderWidth = 1
  82. paper.layer?.masksToBounds = true
  83. addSubview(paper)
  84. NSLayoutConstraint.activate([
  85. paper.topAnchor.constraint(equalTo: topAnchor),
  86. paper.bottomAnchor.constraint(equalTo: bottomAnchor),
  87. paper.centerXAnchor.constraint(equalTo: centerXAnchor),
  88. paper.widthAnchor.constraint(equalTo: heightAnchor, multiplier: 0.82),
  89. paper.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor),
  90. paper.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor)
  91. ])
  92. let root = buildResumeRoot()
  93. root.translatesAutoresizingMaskIntoConstraints = false
  94. paper.addSubview(root)
  95. NSLayoutConstraint.activate([
  96. root.leadingAnchor.constraint(equalTo: paper.leadingAnchor, constant: 7),
  97. root.trailingAnchor.constraint(equalTo: paper.trailingAnchor, constant: -7),
  98. root.topAnchor.constraint(equalTo: paper.topAnchor, constant: 7),
  99. root.bottomAnchor.constraint(lessThanOrEqualTo: paper.bottomAnchor, constant: -7)
  100. ])
  101. }
  102. private func buildResumeRoot() -> NSView {
  103. switch template.family {
  104. case .professional: return buildProfessionalResume()
  105. case .modern: return buildModernResume()
  106. case .minimal: return buildMinimalResume()
  107. case .executive: return buildExecutiveResume()
  108. case .creative: return buildCreativeResume()
  109. }
  110. }
  111. private func paperBackgroundColor() -> NSColor {
  112. switch idVariant % 5 {
  113. case 0: return palette.previewPaper
  114. case 1: return NSColor(srgbRed: 0.995, green: 0.992, blue: 0.985, alpha: 1)
  115. case 2: return NSColor(srgbRed: 0.96, green: 0.99, blue: 1, alpha: 1)
  116. case 3: return NSColor(srgbRed: 0.99, green: 0.99, blue: 0.99, alpha: 1)
  117. default: return NSColor(srgbRed: 0.99, green: 0.98, blue: 0.995, alpha: 1)
  118. }
  119. }
  120. // MARK: - Family: Professional (ATS-friendly)
  121. private func buildProfessionalResume() -> NSView {
  122. let swapExpFirst = (idVariant % 3) == 1
  123. let showRail = (idVariant % 4) == 0
  124. let sidebarMult: CGFloat = (idVariant % 5 == 2) ? 0.38 : 0.34
  125. switch template.layout {
  126. case .singleColumn:
  127. let v = NSStackView()
  128. v.orientation = .vertical
  129. v.spacing = 4 + CGFloat(idVariant % 3)
  130. v.alignment = .leading
  131. v.addArrangedSubview(proHeaderBlock())
  132. if (idVariant % 6) == 4 {
  133. v.addArrangedSubview(proInlineSkillsRow())
  134. }
  135. v.addArrangedSubview(hairline())
  136. let main = proMainColumn(compact: false, experienceFirst: swapExpFirst)
  137. if showRail {
  138. v.addArrangedSubview(horizontalWithLeadingRail(theme: template.themeColor, content: main))
  139. } else {
  140. v.addArrangedSubview(main)
  141. }
  142. return v
  143. case .twoColumn(let side, let tinted):
  144. let bar = proHeaderBlock()
  145. let rule = hairline()
  146. let row = NSStackView()
  147. row.orientation = .horizontal
  148. row.spacing = 5 + CGFloat(idVariant % 3)
  149. row.alignment = .top
  150. let sidebar = proSidebarColumn(tinted: tinted, variant: idVariant)
  151. let main = proMainColumn(compact: true, experienceFirst: swapExpFirst)
  152. if side == .leading {
  153. row.addArrangedSubview(sidebar)
  154. row.addArrangedSubview(main)
  155. } else {
  156. row.addArrangedSubview(main)
  157. row.addArrangedSubview(sidebar)
  158. }
  159. sidebar.widthAnchor.constraint(equalTo: row.widthAnchor, multiplier: sidebarMult).isActive = true
  160. let wrap = NSStackView(views: [bar, rule, row])
  161. wrap.orientation = .vertical
  162. wrap.spacing = 5
  163. wrap.alignment = .leading
  164. return wrap
  165. }
  166. }
  167. private func horizontalWithLeadingRail(theme: NSColor, content: NSView) -> NSView {
  168. let rail = NSView()
  169. rail.translatesAutoresizingMaskIntoConstraints = false
  170. rail.wantsLayer = true
  171. rail.layer?.backgroundColor = theme.cgColor
  172. rail.layer?.cornerRadius = 1
  173. rail.widthAnchor.constraint(equalToConstant: 3).isActive = true
  174. let row = NSStackView(views: [rail, content])
  175. row.orientation = .horizontal
  176. row.spacing = 7
  177. row.alignment = .top
  178. return row
  179. }
  180. private func proInlineSkillsRow() -> NSView {
  181. let joined = CVPreviewDemoContent.skillsList.prefix(4).joined(separator: " · ")
  182. return makeLabel(joined, font: .systemFont(ofSize: 5.8, weight: .medium), color: template.themeColor, alignment: .left, maxLines: 2)
  183. }
  184. private func proHeaderBlock() -> NSView {
  185. let theme = template.themeColor
  186. let ink = palette.previewInk
  187. let muted = palette.previewMuted
  188. let name = makeLabel(CVPreviewDemoContent.fullName, font: .systemFont(ofSize: 9.5, weight: .semibold), color: ink, alignment: .left, maxLines: 1)
  189. let role = makeLabel(CVPreviewDemoContent.title, font: .systemFont(ofSize: 7.4, weight: .regular), color: muted, alignment: .left, maxLines: 1)
  190. let contact = makeLabel("\(CVPreviewDemoContent.email) · \(CVPreviewDemoContent.phone)", font: .systemFont(ofSize: 6.2, weight: .regular), color: muted.withAlphaComponent(0.88), alignment: .left, maxLines: 1)
  191. let textCol = NSStackView(views: [name, role, contact])
  192. textCol.orientation = .vertical
  193. textCol.spacing = 2
  194. textCol.alignment = .leading
  195. let row = NSStackView()
  196. row.orientation = .horizontal
  197. row.spacing = 6
  198. row.alignment = .centerY
  199. switch template.headline {
  200. case .centered:
  201. textCol.alignment = .centerX
  202. name.alignment = .center
  203. role.alignment = .center
  204. contact.alignment = .center
  205. let accent = headlineAccent(theme: theme, width: 0.42)
  206. let stack = NSStackView(views: [textCol, accent])
  207. stack.orientation = .vertical
  208. stack.spacing = 4
  209. stack.alignment = .centerX
  210. return stack
  211. case .leftAligned:
  212. row.addArrangedSubview(textCol)
  213. let col = NSStackView(views: [row, headlineAccent(theme: theme, width: 0.38)])
  214. col.orientation = .vertical
  215. col.spacing = 4
  216. col.alignment = .leading
  217. return col
  218. case .leftWithInitials:
  219. let avatar = initialsAvatar(diameter: 22, ink: ink)
  220. row.addArrangedSubview(textCol)
  221. row.addArrangedSubview(NSView()) // spacer
  222. row.addArrangedSubview(avatar)
  223. let col = NSStackView(views: [row, headlineAccent(theme: theme, width: 0.36)])
  224. col.orientation = .vertical
  225. col.spacing = 4
  226. col.alignment = .leading
  227. return col
  228. case .avatarStacked:
  229. let avatar = initialsAvatar(diameter: 24, ink: ink)
  230. let stack = NSStackView()
  231. stack.orientation = .vertical
  232. stack.spacing = 3
  233. stack.alignment = .centerX
  234. stack.addArrangedSubview(avatar)
  235. textCol.alignment = .centerX
  236. name.alignment = .center
  237. role.alignment = .center
  238. contact.alignment = .center
  239. stack.addArrangedSubview(textCol)
  240. stack.addArrangedSubview(headlineAccent(theme: theme, width: 0.4))
  241. return stack
  242. }
  243. }
  244. private func proSidebarColumn(tinted: Bool, variant: Int) -> NSView {
  245. let box = NSStackView()
  246. box.orientation = .vertical
  247. box.spacing = 4 + CGFloat(variant % 3)
  248. box.alignment = .leading
  249. if tinted {
  250. box.wantsLayer = true
  251. let tint = (variant % 4 == 1)
  252. ? template.themeColor.withAlphaComponent(0.08)
  253. : palette.previewSidebarTint
  254. box.layer?.backgroundColor = tint.cgColor
  255. box.layer?.cornerRadius = variant % 3 == 0 ? 5 : 4
  256. }
  257. let pad: CGFloat = tinted ? 5 : 0
  258. let inner = NSStackView()
  259. inner.edgeInsets = NSEdgeInsets(top: pad, left: pad, bottom: pad, right: pad)
  260. inner.orientation = .vertical
  261. inner.spacing = 5
  262. inner.alignment = .leading
  263. inner.addArrangedSubview(sectionHeading("CONTACT"))
  264. inner.addArrangedSubview(makeLabel(CVPreviewDemoContent.email, font: .systemFont(ofSize: 6.2), color: palette.previewInk, alignment: .left, maxLines: 2))
  265. inner.addArrangedSubview(makeLabel(CVPreviewDemoContent.phone, font: .systemFont(ofSize: 6.2), color: palette.previewMuted, alignment: .left, maxLines: 1))
  266. inner.addArrangedSubview(makeLabel(CVPreviewDemoContent.location, font: .systemFont(ofSize: 6.2), color: palette.previewMuted, alignment: .left, maxLines: 1))
  267. inner.addArrangedSubview(sectionHeading("SKILLS"))
  268. if variant % 5 == 2 {
  269. inner.addArrangedSubview(tagRow(theme: template.themeColor))
  270. } else {
  271. for s in CVPreviewDemoContent.skillsList.prefix(4) {
  272. inner.addArrangedSubview(skillLineBullet(s))
  273. }
  274. }
  275. if variant % 7 == 3 {
  276. inner.addArrangedSubview(sectionHeading("TOOLS"))
  277. inner.addArrangedSubview(makeLabel("Amplitude · Jira · BigQuery", font: .systemFont(ofSize: 5.9), color: palette.previewMuted, alignment: .left, maxLines: 1))
  278. }
  279. box.addArrangedSubview(inner)
  280. return box
  281. }
  282. private func proMainColumn(compact: Bool, experienceFirst: Bool) -> NSView {
  283. let stack = NSStackView()
  284. stack.orientation = .vertical
  285. stack.spacing = compact ? 4 : 5 + CGFloat(idVariant % 2)
  286. stack.alignment = .leading
  287. let sp: CGFloat = compact ? 6.2 : 6.5
  288. let profileBlock: () -> Void = {
  289. stack.addArrangedSubview(self.sectionHeading("PROFILE"))
  290. stack.addArrangedSubview(self.makeLabel(CVPreviewDemoContent.summary, font: .systemFont(ofSize: sp), color: self.palette.previewInk, alignment: .left, maxLines: 3))
  291. }
  292. let experienceBlock: () -> Void = {
  293. stack.addArrangedSubview(self.sectionHeading("EXPERIENCE"))
  294. stack.addArrangedSubview(self.makeLabel(CVPreviewDemoContent.title, font: .systemFont(ofSize: 6.8, weight: .semibold), color: self.palette.previewInk, alignment: .left, maxLines: 1))
  295. stack.addArrangedSubview(self.makeLabel(CVPreviewDemoContent.companyLine, font: .systemFont(ofSize: 6.2, weight: .medium), color: self.template.themeColor, alignment: .left, maxLines: 1))
  296. stack.addArrangedSubview(self.bulletRow(CVPreviewDemoContent.bullet1, size: sp))
  297. stack.addArrangedSubview(self.bulletRow(CVPreviewDemoContent.bullet2, size: sp))
  298. }
  299. if experienceFirst {
  300. experienceBlock()
  301. profileBlock()
  302. } else {
  303. profileBlock()
  304. experienceBlock()
  305. }
  306. stack.addArrangedSubview(sectionHeading("EDUCATION"))
  307. stack.addArrangedSubview(makeLabel(CVPreviewDemoContent.university, font: .systemFont(ofSize: 6.6, weight: .semibold), color: palette.previewInk, alignment: .left, maxLines: 1))
  308. stack.addArrangedSubview(makeLabel(CVPreviewDemoContent.degree, font: .systemFont(ofSize: 6.2), color: palette.previewMuted, alignment: .left, maxLines: 2))
  309. return stack
  310. }
  311. // MARK: - Family: Modern (three distinct silhouettes per id)
  312. private func buildModernResume() -> NSView {
  313. switch idVariant % 3 {
  314. case 0: return buildModernClassicBandLayout()
  315. case 1: return buildModernRailDocLayout()
  316. default: return buildModernSplitHeaderLayout()
  317. }
  318. }
  319. private func modernPrimaryBody(theme: NSColor) -> NSView {
  320. switch template.layout {
  321. case .singleColumn:
  322. return modernBodySingleColumn(theme: theme)
  323. case .twoColumn(let side, let tinted):
  324. let main = modernBodySingleColumn(theme: theme)
  325. let sideCol = modernSidebar(theme: theme, tinted: tinted)
  326. let row = NSStackView()
  327. row.orientation = .horizontal
  328. row.spacing = 5 + CGFloat(idVariant % 3)
  329. row.alignment = .top
  330. if side == .leading {
  331. row.addArrangedSubview(sideCol)
  332. row.addArrangedSubview(main)
  333. } else {
  334. row.addArrangedSubview(main)
  335. row.addArrangedSubview(sideCol)
  336. }
  337. let mult: CGFloat = (idVariant % 4 == 2) ? 0.36 : 0.32
  338. sideCol.widthAnchor.constraint(equalTo: row.widthAnchor, multiplier: mult).isActive = true
  339. return row
  340. }
  341. }
  342. private func buildModernClassicBandLayout() -> NSView {
  343. let theme = template.themeColor
  344. let header = NSView()
  345. header.translatesAutoresizingMaskIntoConstraints = false
  346. header.wantsLayer = true
  347. header.layer?.backgroundColor = theme.cgColor
  348. header.layer?.cornerRadius = idVariant % 2 == 0 ? 5 : 3
  349. let white = NSColor.white
  350. let name = makeLabel(CVPreviewDemoContent.fullName, font: .systemFont(ofSize: 9, weight: .bold), color: white, alignment: .left, maxLines: 1)
  351. let role = makeLabel(CVPreviewDemoContent.title, font: .systemFont(ofSize: 7.2, weight: .medium), color: white.withAlphaComponent(0.92), alignment: .left, maxLines: 1)
  352. let hstack = NSStackView(views: [name, role])
  353. hstack.orientation = .vertical
  354. hstack.spacing = 2
  355. hstack.alignment = .leading
  356. hstack.translatesAutoresizingMaskIntoConstraints = false
  357. let iconRow = NSStackView()
  358. iconRow.orientation = .horizontal
  359. iconRow.spacing = 5
  360. iconRow.translatesAutoresizingMaskIntoConstraints = false
  361. for sym in ["mappin.and.ellipse", "phone.fill", "envelope.fill"] {
  362. guard let img = NSImage(systemSymbolName: sym, accessibilityDescription: nil) else { continue }
  363. let iv = NSImageView(image: img)
  364. iv.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 6, weight: .semibold)
  365. iv.contentTintColor = white.withAlphaComponent(0.85)
  366. iconRow.addArrangedSubview(iv)
  367. }
  368. let topRow = NSStackView()
  369. topRow.orientation = .horizontal
  370. topRow.spacing = 8
  371. topRow.alignment = .centerY
  372. topRow.translatesAutoresizingMaskIntoConstraints = false
  373. topRow.addArrangedSubview(hstack)
  374. topRow.addArrangedSubview(NSView())
  375. topRow.addArrangedSubview(iconRow)
  376. header.addSubview(topRow)
  377. NSLayoutConstraint.activate([
  378. topRow.leadingAnchor.constraint(equalTo: header.leadingAnchor, constant: 7),
  379. topRow.trailingAnchor.constraint(equalTo: header.trailingAnchor, constant: -7),
  380. topRow.topAnchor.constraint(equalTo: header.topAnchor, constant: 6),
  381. topRow.bottomAnchor.constraint(equalTo: header.bottomAnchor, constant: -6),
  382. header.heightAnchor.constraint(greaterThanOrEqualToConstant: 32 + CGFloat(idVariant % 3) * 2)
  383. ])
  384. let body = modernPrimaryBody(theme: theme)
  385. let wrap = NSStackView(views: [header, body])
  386. wrap.orientation = .vertical
  387. wrap.spacing = 6 + CGFloat(idVariant % 2)
  388. wrap.alignment = .leading
  389. return wrap
  390. }
  391. private func buildModernRailDocLayout() -> NSView {
  392. let theme = template.themeColor
  393. let ink = palette.previewInk
  394. let muted = palette.previewMuted
  395. let rail = NSView()
  396. rail.translatesAutoresizingMaskIntoConstraints = false
  397. rail.wantsLayer = true
  398. rail.layer?.backgroundColor = theme.cgColor
  399. rail.layer?.cornerRadius = 2
  400. rail.widthAnchor.constraint(equalToConstant: 3 + CGFloat(idVariant % 2)).isActive = true
  401. let inner = NSStackView()
  402. inner.orientation = .vertical
  403. inner.spacing = 5
  404. inner.alignment = .leading
  405. inner.addArrangedSubview(makeLabel(CVPreviewDemoContent.fullName, font: .systemFont(ofSize: 9.5, weight: .bold), color: ink, alignment: .left, maxLines: 1))
  406. inner.addArrangedSubview(makeLabel(CVPreviewDemoContent.title, font: .systemFont(ofSize: 7.2, weight: .semibold), color: theme, alignment: .left, maxLines: 1))
  407. inner.addArrangedSubview(makeLabel("\(CVPreviewDemoContent.email) · \(CVPreviewDemoContent.phone)", font: .systemFont(ofSize: 6), color: muted, alignment: .left, maxLines: 1))
  408. inner.addArrangedSubview(hairline())
  409. inner.addArrangedSubview(tagRow(theme: theme))
  410. inner.addArrangedSubview(modernPrimaryBody(theme: theme))
  411. let row = NSStackView(views: [rail, inner])
  412. row.orientation = .horizontal
  413. row.spacing = 7
  414. row.alignment = .top
  415. return row
  416. }
  417. private func buildModernSplitHeaderLayout() -> NSView {
  418. let theme = template.themeColor
  419. let ink = palette.previewInk
  420. let muted = palette.previewMuted
  421. let left = NSStackView()
  422. left.orientation = .vertical
  423. left.spacing = 3
  424. left.alignment = .leading
  425. left.addArrangedSubview(makeLabel(CVPreviewDemoContent.fullName, font: .systemFont(ofSize: 9.2, weight: .bold), color: ink, alignment: .left, maxLines: 2))
  426. left.addArrangedSubview(makeLabel(CVPreviewDemoContent.title, font: .systemFont(ofSize: 7, weight: .medium), color: muted, alignment: .left, maxLines: 2))
  427. left.addArrangedSubview(makeLabel(CVPreviewDemoContent.location, font: .systemFont(ofSize: 6), color: muted.withAlphaComponent(0.85), alignment: .left, maxLines: 1))
  428. let right = NSStackView()
  429. right.orientation = .vertical
  430. right.spacing = 4
  431. right.alignment = .leading
  432. right.wantsLayer = true
  433. right.layer?.backgroundColor = theme.cgColor
  434. right.layer?.cornerRadius = 5
  435. right.edgeInsets = NSEdgeInsets(top: 6, left: 7, bottom: 6, right: 7)
  436. let onW = NSColor.white
  437. right.addArrangedSubview(makeLabel(CVPreviewDemoContent.email, font: .systemFont(ofSize: 5.9, weight: .medium), color: onW.withAlphaComponent(0.95), alignment: .left, maxLines: 2))
  438. right.addArrangedSubview(makeLabel(CVPreviewDemoContent.phone, font: .systemFont(ofSize: 5.9, weight: .medium), color: onW.withAlphaComponent(0.9), alignment: .left, maxLines: 1))
  439. right.addArrangedSubview(makeLabel("Open to relocation", font: .systemFont(ofSize: 5.6, weight: .regular), color: onW.withAlphaComponent(0.75), alignment: .left, maxLines: 1))
  440. let top = NSStackView(views: [left, right])
  441. top.orientation = .horizontal
  442. top.spacing = 6
  443. top.alignment = .top
  444. left.widthAnchor.constraint(equalTo: top.widthAnchor, multiplier: 0.54).isActive = true
  445. let col = NSStackView(views: [top, hairline(), modernPrimaryBody(theme: theme)])
  446. col.orientation = .vertical
  447. col.spacing = 7
  448. col.alignment = .leading
  449. return col
  450. }
  451. private func modernSidebar(theme: NSColor, tinted: Bool) -> NSView {
  452. let box = NSStackView()
  453. box.orientation = .vertical
  454. box.spacing = 5
  455. box.alignment = .leading
  456. if tinted {
  457. box.wantsLayer = true
  458. box.layer?.backgroundColor = theme.withAlphaComponent(0.1).cgColor
  459. box.layer?.cornerRadius = 4
  460. }
  461. box.edgeInsets = NSEdgeInsets(top: 4, left: 4, bottom: 4, right: 4)
  462. box.addArrangedSubview(modernSectionRow(symbol: "person.crop.circle", title: "About", theme: theme))
  463. box.addArrangedSubview(makeLabel(CVPreviewDemoContent.summary, font: .systemFont(ofSize: 6), color: palette.previewInk, alignment: .left, maxLines: 4))
  464. box.addArrangedSubview(modernSectionRow(symbol: "star.fill", title: "Highlights", theme: theme))
  465. box.addArrangedSubview(makeLabel("Launched growth experiments, SQL deep dives, design QA.", font: .systemFont(ofSize: 6), color: palette.previewMuted, alignment: .left, maxLines: 3))
  466. return box
  467. }
  468. private func modernSectionRow(symbol: String, title: String, theme: NSColor) -> NSView {
  469. guard let img = NSImage(systemSymbolName: symbol, accessibilityDescription: nil) else {
  470. return makeLabel(title, font: .systemFont(ofSize: 6.5, weight: .bold), color: palette.previewInk, alignment: .left, maxLines: 1)
  471. }
  472. let iv = NSImageView(image: img)
  473. iv.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 6, weight: .semibold)
  474. iv.contentTintColor = theme
  475. let t = makeLabel(title, font: .systemFont(ofSize: 6.5, weight: .bold), color: palette.previewInk, alignment: .left, maxLines: 1)
  476. let r = NSStackView(views: [iv, t])
  477. r.orientation = .horizontal
  478. r.spacing = 4
  479. r.alignment = .centerY
  480. return r
  481. }
  482. private func modernBodySingleColumn(theme: NSColor) -> NSView {
  483. let stack = NSStackView()
  484. stack.orientation = .vertical
  485. stack.spacing = 5
  486. stack.alignment = .leading
  487. stack.addArrangedSubview(modernSectionRow(symbol: "briefcase.fill", title: "Experience", theme: theme))
  488. stack.addArrangedSubview(makeLabel("\(CVPreviewDemoContent.title) — \(CVPreviewDemoContent.company)", font: .systemFont(ofSize: 6.6, weight: .semibold), color: palette.previewInk, alignment: .left, maxLines: 2))
  489. stack.addArrangedSubview(makeLabel("2019 – Present · Led cross-functional pods from discovery to launch.", font: .systemFont(ofSize: 6.1), color: palette.previewMuted, alignment: .left, maxLines: 2))
  490. stack.addArrangedSubview(tagRow(theme: theme))
  491. stack.addArrangedSubview(modernSectionRow(symbol: "graduationcap.fill", title: "Education", theme: theme))
  492. stack.addArrangedSubview(makeLabel("\(CVPreviewDemoContent.university), \(CVPreviewDemoContent.degree)", font: .systemFont(ofSize: 6.2), color: palette.previewInk, alignment: .left, maxLines: 2))
  493. return stack
  494. }
  495. private func tagRow(theme: NSColor) -> NSView {
  496. let row = NSStackView()
  497. row.orientation = .horizontal
  498. row.spacing = 3
  499. row.alignment = .centerY
  500. for s in CVPreviewDemoContent.skillsList.prefix(4) {
  501. let tag = NSView()
  502. tag.wantsLayer = true
  503. tag.layer?.backgroundColor = theme.withAlphaComponent(0.14).cgColor
  504. tag.layer?.cornerRadius = 3
  505. tag.translatesAutoresizingMaskIntoConstraints = false
  506. let lab = makeLabel(s, font: .systemFont(ofSize: 5.5, weight: .semibold), color: theme.blended(withFraction: 0.35, of: palette.previewInk) ?? palette.previewInk, alignment: .center, maxLines: 1)
  507. lab.translatesAutoresizingMaskIntoConstraints = false
  508. tag.addSubview(lab)
  509. NSLayoutConstraint.activate([
  510. lab.leadingAnchor.constraint(equalTo: tag.leadingAnchor, constant: 4),
  511. lab.trailingAnchor.constraint(equalTo: tag.trailingAnchor, constant: -4),
  512. lab.topAnchor.constraint(equalTo: tag.topAnchor, constant: 2),
  513. lab.bottomAnchor.constraint(equalTo: tag.bottomAnchor, constant: -2)
  514. ])
  515. row.addArrangedSubview(tag)
  516. }
  517. return row
  518. }
  519. // MARK: - Family: Minimal
  520. private func buildMinimalResume() -> NSView {
  521. let ink = palette.previewInk
  522. let muted = palette.previewMuted
  523. let nameWeight: NSFont.Weight = (idVariant % 3 == 0) ? .ultraLight : .light
  524. let nameSize: CGFloat = template.headline == .centered ? 11 + CGFloat(idVariant % 2) : 9.5 + CGFloat(idVariant % 3)
  525. let name = makeLabel(CVPreviewDemoContent.fullName, font: .systemFont(ofSize: nameSize, weight: nameWeight), color: ink, alignment: .left, maxLines: 1)
  526. let role = makeLabel(CVPreviewDemoContent.title.uppercased(), font: .systemFont(ofSize: 6.5, weight: .medium), color: muted, alignment: .left, maxLines: 1)
  527. let contact = makeLabel("\(CVPreviewDemoContent.email) \(CVPreviewDemoContent.phone)", font: .systemFont(ofSize: 5.8, weight: .regular), color: muted.withAlphaComponent(0.75), alignment: .left, maxLines: 1)
  528. let head = NSStackView()
  529. head.orientation = .vertical
  530. head.spacing = template.headline == .avatarStacked ? 8 : 4 + CGFloat(idVariant % 4)
  531. head.alignment = template.headline == .centered ? .centerX : .leading
  532. if template.headline == .avatarStacked {
  533. head.addArrangedSubview(initialsAvatar(diameter: 24 + CGFloat(idVariant % 2) * 2, ink: ink))
  534. }
  535. if template.headline == .centered {
  536. name.alignment = .center
  537. role.alignment = .center
  538. contact.alignment = .center
  539. }
  540. head.addArrangedSubview(name)
  541. head.addArrangedSubview(role)
  542. head.addArrangedSubview(contact)
  543. head.addArrangedSubview(hairlineSoft())
  544. if idVariant % 5 == 1 {
  545. head.addArrangedSubview(hairlineSoft())
  546. }
  547. let swapEdu = (idVariant % 4) == 2
  548. let body: NSView
  549. switch template.layout {
  550. case .singleColumn:
  551. body = minimalBody(spacing: 6 + CGFloat(idVariant % 3), educationBeforeExperience: swapEdu)
  552. case .twoColumn(let side, _):
  553. let a = minimalBody(spacing: 5, educationBeforeExperience: swapEdu)
  554. let b = minimalAside(numbered: idVariant % 3 == 1)
  555. let row = NSStackView()
  556. row.orientation = .horizontal
  557. row.spacing = 8 + CGFloat(idVariant % 3)
  558. row.alignment = .top
  559. if side == .leading {
  560. row.addArrangedSubview(b)
  561. row.addArrangedSubview(a)
  562. } else {
  563. row.addArrangedSubview(a)
  564. row.addArrangedSubview(b)
  565. }
  566. let mult: CGFloat = (idVariant % 5 == 0) ? 0.34 : 0.3
  567. b.widthAnchor.constraint(equalTo: row.widthAnchor, multiplier: mult).isActive = true
  568. body = row
  569. }
  570. let wrap = NSStackView(views: [head, body])
  571. wrap.orientation = .vertical
  572. wrap.spacing = 7 + CGFloat(idVariant % 2)
  573. wrap.alignment = .leading
  574. return wrap
  575. }
  576. private func minimalBody(spacing: CGFloat, educationBeforeExperience: Bool) -> NSView {
  577. let stack = NSStackView()
  578. stack.orientation = .vertical
  579. stack.spacing = spacing
  580. stack.alignment = .leading
  581. let edu: () -> Void = {
  582. stack.addArrangedSubview(self.sectionHeading("EDUCATION"))
  583. stack.addArrangedSubview(self.makeLabel("\(CVPreviewDemoContent.university) — \(CVPreviewDemoContent.degree)", font: .systemFont(ofSize: 6.1, weight: .light), color: self.palette.previewInk, alignment: .left, maxLines: 2))
  584. }
  585. let prof: () -> Void = {
  586. stack.addArrangedSubview(self.sectionHeading("PROFILE"))
  587. stack.addArrangedSubview(self.makeLabel(CVPreviewDemoContent.summary, font: .systemFont(ofSize: 6.3, weight: .light), color: self.palette.previewInk, alignment: .left, maxLines: 3))
  588. }
  589. let exp: () -> Void = {
  590. stack.addArrangedSubview(self.sectionHeading("EXPERIENCE"))
  591. stack.addArrangedSubview(self.makeLabel("\(CVPreviewDemoContent.title), \(CVPreviewDemoContent.company)", font: .systemFont(ofSize: 6.5, weight: .regular), color: self.palette.previewInk, alignment: .left, maxLines: 2))
  592. stack.addArrangedSubview(self.makeLabel(CVPreviewDemoContent.bullet1, font: .systemFont(ofSize: 6, weight: .light), color: self.palette.previewMuted, alignment: .left, maxLines: 2))
  593. }
  594. if educationBeforeExperience {
  595. edu()
  596. prof()
  597. exp()
  598. } else {
  599. prof()
  600. exp()
  601. edu()
  602. }
  603. return stack
  604. }
  605. private func minimalAside(numbered: Bool) -> NSView {
  606. let stack = NSStackView()
  607. stack.orientation = .vertical
  608. stack.spacing = 6
  609. stack.alignment = .leading
  610. stack.addArrangedSubview(sectionHeading("SKILLS"))
  611. for (i, s) in CVPreviewDemoContent.skillsList.enumerated() {
  612. let prefix = numbered ? "\(i + 1). " : "· "
  613. stack.addArrangedSubview(makeLabel("\(prefix)\(s)", font: .systemFont(ofSize: 6, weight: .light), color: palette.previewMuted, alignment: .left, maxLines: 1))
  614. }
  615. return stack
  616. }
  617. // MARK: - Family: Executive (serif)
  618. private func buildExecutiveResume() -> NSView {
  619. let serif = NSFont(name: "Georgia", size: 6.4) ?? .systemFont(ofSize: 6.4)
  620. let serifBase = NSFont(name: "Georgia", size: 7.8) ?? .systemFont(ofSize: 7.8)
  621. let serifBold = NSFontManager.shared.convert(serifBase, toHaveTrait: .boldFontMask)
  622. let ink = palette.previewInk
  623. let muted = palette.previewMuted
  624. let theme = template.themeColor
  625. let centeredHead = (idVariant % 2) == 0
  626. let name = makeLabel(CVPreviewDemoContent.fullName, font: serifBold, color: ink, alignment: centeredHead ? .center : .left, maxLines: 1)
  627. let role = makeLabel(CVPreviewDemoContent.title, font: serif, color: muted, alignment: centeredHead ? .center : .left, maxLines: 1)
  628. let contact = makeLabel("\(CVPreviewDemoContent.email) · \(CVPreviewDemoContent.phone) · \(CVPreviewDemoContent.location)", font: NSFont(name: "Georgia", size: 5.8) ?? serif, color: muted.withAlphaComponent(0.9), alignment: centeredHead ? .center : .left, maxLines: 2)
  629. let rule = executiveRule(theme: theme, wide: idVariant % 3 == 0)
  630. let head = NSStackView(views: [name, role, contact, rule])
  631. head.orientation = .vertical
  632. head.spacing = 4
  633. head.alignment = centeredHead ? .centerX : .leading
  634. let body: NSView
  635. switch template.layout {
  636. case .singleColumn:
  637. body = executiveBody(serif: serif, ink: ink, muted: muted, theme: theme, compact: false, tightLeading: idVariant % 5 == 2)
  638. case .twoColumn(let side, let tinted):
  639. let main = executiveBody(serif: serif, ink: ink, muted: muted, theme: theme, compact: true, tightLeading: idVariant % 5 == 2)
  640. let sideC = executiveSidebar(serif: serif, tinted: tinted, showMetrics: idVariant % 4 == 1)
  641. let row = NSStackView()
  642. row.orientation = .horizontal
  643. row.spacing = 6 + CGFloat(idVariant % 3)
  644. row.alignment = .top
  645. if side == .leading {
  646. row.addArrangedSubview(sideC)
  647. row.addArrangedSubview(main)
  648. } else {
  649. row.addArrangedSubview(main)
  650. row.addArrangedSubview(sideC)
  651. }
  652. let mult: CGFloat = (idVariant % 5 == 3) ? 0.38 : 0.33
  653. sideC.widthAnchor.constraint(equalTo: row.widthAnchor, multiplier: mult).isActive = true
  654. body = row
  655. }
  656. let wrap = NSStackView(views: [head, body])
  657. wrap.orientation = .vertical
  658. wrap.spacing = 6 + CGFloat(idVariant % 2)
  659. wrap.alignment = .leading
  660. return wrap
  661. }
  662. private func executiveRule(theme: NSColor, wide: Bool) -> NSView {
  663. let v = NSView()
  664. v.translatesAutoresizingMaskIntoConstraints = false
  665. v.wantsLayer = true
  666. v.layer?.backgroundColor = theme.withAlphaComponent(0.45).cgColor
  667. v.heightAnchor.constraint(equalToConstant: wide ? 2 : 1).isActive = true
  668. v.widthAnchor.constraint(equalToConstant: wide ? 160 : 110).isActive = true
  669. return v
  670. }
  671. private func executiveBody(serif: NSFont, ink: NSColor, muted: NSColor, theme: NSColor, compact: Bool, tightLeading: Bool) -> NSView {
  672. let stack = NSStackView()
  673. stack.orientation = .vertical
  674. stack.spacing = (compact ? 4 : 5) - (tightLeading ? 1 : 0)
  675. stack.alignment = .leading
  676. let sumTitle = (idVariant % 6 == 3) ? "SUMMARY" : "PROFESSIONAL SUMMARY"
  677. stack.addArrangedSubview(sectionHeading(sumTitle))
  678. stack.addArrangedSubview(makeLabel(CVPreviewDemoContent.summary, font: serif, color: ink, alignment: .left, maxLines: 3))
  679. stack.addArrangedSubview(sectionHeading("SELECTED EXPERIENCE"))
  680. let jobFont = NSFontManager.shared.convert(serif, toHaveTrait: .boldFontMask)
  681. stack.addArrangedSubview(makeLabel("\(CVPreviewDemoContent.title), \(CVPreviewDemoContent.company)", font: jobFont, color: ink, alignment: .left, maxLines: 2))
  682. stack.addArrangedSubview(makeLabel("2019 – Present", font: serif, color: theme, alignment: .left, maxLines: 1))
  683. stack.addArrangedSubview(makeLabel(CVPreviewDemoContent.bullet1, font: serif, color: muted, alignment: .left, maxLines: 2))
  684. stack.addArrangedSubview(sectionHeading("EDUCATION"))
  685. stack.addArrangedSubview(makeLabel("\(CVPreviewDemoContent.university), \(CVPreviewDemoContent.degree)", font: serif, color: ink, alignment: .left, maxLines: 2))
  686. return stack
  687. }
  688. private func executiveSidebar(serif: NSFont, tinted: Bool, showMetrics: Bool) -> NSView {
  689. let stack = NSStackView()
  690. stack.orientation = .vertical
  691. stack.spacing = 4
  692. stack.alignment = .leading
  693. if tinted {
  694. stack.wantsLayer = true
  695. let fill = (idVariant % 3 == 0)
  696. ? NSColor(srgbRed: 0.97, green: 0.97, blue: 0.98, alpha: 1)
  697. : template.themeColor.withAlphaComponent(0.07)
  698. stack.layer?.backgroundColor = fill.cgColor
  699. stack.layer?.cornerRadius = idVariant % 4 == 2 ? 5 : 3
  700. stack.edgeInsets = NSEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
  701. }
  702. stack.addArrangedSubview(sectionHeading("CORE COMPETENCIES"))
  703. for s in CVPreviewDemoContent.skillsList {
  704. stack.addArrangedSubview(makeLabel("· \(s)", font: serif, color: palette.previewInk, alignment: .left, maxLines: 1))
  705. }
  706. stack.addArrangedSubview(sectionHeading("TOOLS"))
  707. stack.addArrangedSubview(makeLabel("Figma, SQL, Amplitude, Jira", font: serif, color: palette.previewMuted, alignment: .left, maxLines: 2))
  708. if showMetrics {
  709. stack.addArrangedSubview(sectionHeading("IMPACT"))
  710. stack.addArrangedSubview(makeLabel("+12% activation · $4.2M ARR influenced", font: serif, color: palette.previewInk, alignment: .left, maxLines: 2))
  711. }
  712. return stack
  713. }
  714. // MARK: - Family: Creative (sidebar + bold accent)
  715. private func buildCreativeResume() -> NSView {
  716. let theme = template.themeColor
  717. let deep = creativeDeepBackground(theme: theme)
  718. let onSidebar = NSColor.white.withAlphaComponent(0.95)
  719. let skillPrefix = (idVariant % 3 == 0) ? "• " : "▸ "
  720. let sidebar = NSStackView()
  721. sidebar.orientation = .vertical
  722. sidebar.spacing = 4 + CGFloat(idVariant % 3)
  723. sidebar.alignment = .leading
  724. sidebar.wantsLayer = true
  725. sidebar.layer?.backgroundColor = deep.cgColor
  726. sidebar.layer?.cornerRadius = idVariant % 2 == 0 ? 6 : 4
  727. sidebar.edgeInsets = NSEdgeInsets(top: 6, left: 6, bottom: 6, right: 6)
  728. let sbTitle = makeLabel(CVPreviewDemoContent.fullName, font: .systemFont(ofSize: 7.5 + CGFloat(idVariant % 2), weight: .bold), color: onSidebar, alignment: .left, maxLines: 2)
  729. sidebar.addArrangedSubview(sbTitle)
  730. sidebar.addArrangedSubview(makeLabel(CVPreviewDemoContent.title, font: .systemFont(ofSize: 6.5, weight: .medium), color: onSidebar.withAlphaComponent(0.85), alignment: .left, maxLines: 2))
  731. sidebar.addArrangedSubview(makeLabel(CVPreviewDemoContent.email, font: .systemFont(ofSize: 5.8), color: onSidebar.withAlphaComponent(0.8), alignment: .left, maxLines: 1))
  732. sidebar.addArrangedSubview(makeLabel(CVPreviewDemoContent.phone, font: .systemFont(ofSize: 5.8), color: onSidebar.withAlphaComponent(0.8), alignment: .left, maxLines: 1))
  733. sidebar.addArrangedSubview(creativeSidebarHeading("STRENGTHS", onSidebar: onSidebar, accent: theme))
  734. for s in CVPreviewDemoContent.skillsList.prefix(5) {
  735. sidebar.addArrangedSubview(makeLabel("\(skillPrefix)\(s)", font: .systemFont(ofSize: 6, weight: .semibold), color: onSidebar.withAlphaComponent(0.92), alignment: .left, maxLines: 1))
  736. }
  737. let main = NSStackView()
  738. main.orientation = .vertical
  739. main.spacing = 4 + CGFloat(idVariant % 3)
  740. main.alignment = .leading
  741. main.addArrangedSubview(creativeMainHeader(theme: theme))
  742. main.addArrangedSubview(sectionHeading("PROFILE"))
  743. main.addArrangedSubview(makeLabel(CVPreviewDemoContent.summary, font: .systemFont(ofSize: 6.2), color: palette.previewInk, alignment: .left, maxLines: 3))
  744. main.addArrangedSubview(sectionHeading("IMPACT"))
  745. main.addArrangedSubview(makeLabel("\(CVPreviewDemoContent.company) — \(CVPreviewDemoContent.title)", font: .systemFont(ofSize: 6.6, weight: .heavy), color: palette.previewInk, alignment: .left, maxLines: 2))
  746. let bMark = (idVariant % 2 == 0) ? "— " : "▸ "
  747. main.addArrangedSubview(makeLabel("\(bMark)\(CVPreviewDemoContent.bullet1)", font: .systemFont(ofSize: 6), color: palette.previewMuted, alignment: .left, maxLines: 2))
  748. main.addArrangedSubview(makeLabel("\(bMark)\(CVPreviewDemoContent.bullet2)", font: .systemFont(ofSize: 6), color: palette.previewMuted, alignment: .left, maxLines: 2))
  749. let sidebarMult = 0.32 + CGFloat(idVariant % 3) * 0.02
  750. switch template.layout {
  751. case .singleColumn:
  752. let banner = NSView()
  753. banner.translatesAutoresizingMaskIntoConstraints = false
  754. banner.wantsLayer = true
  755. banner.layer?.backgroundColor = theme.cgColor
  756. banner.layer?.cornerRadius = idVariant % 4 == 1 ? 6 : 3
  757. let inner = makeLabel(" \(CVPreviewDemoContent.fullName) · \(CVPreviewDemoContent.title)", font: .systemFont(ofSize: 6.5, weight: .bold), color: .white, alignment: .left, maxLines: 1)
  758. inner.translatesAutoresizingMaskIntoConstraints = false
  759. banner.addSubview(inner)
  760. NSLayoutConstraint.activate([
  761. inner.leadingAnchor.constraint(equalTo: banner.leadingAnchor, constant: 5),
  762. inner.trailingAnchor.constraint(lessThanOrEqualTo: banner.trailingAnchor, constant: -5),
  763. inner.topAnchor.constraint(equalTo: banner.topAnchor, constant: 4 + CGFloat(idVariant % 2)),
  764. inner.bottomAnchor.constraint(equalTo: banner.bottomAnchor, constant: -4)
  765. ])
  766. let col = NSStackView(views: [banner, main])
  767. col.orientation = .vertical
  768. col.spacing = 6
  769. col.alignment = .leading
  770. return col
  771. case .twoColumn(let side, _):
  772. let row = NSStackView()
  773. row.orientation = .horizontal
  774. row.spacing = 5 + CGFloat(idVariant % 3)
  775. row.alignment = .top
  776. if side == .leading {
  777. row.addArrangedSubview(sidebar)
  778. row.addArrangedSubview(main)
  779. } else {
  780. row.addArrangedSubview(main)
  781. row.addArrangedSubview(sidebar)
  782. }
  783. sidebar.widthAnchor.constraint(equalTo: row.widthAnchor, multiplier: sidebarMult).isActive = true
  784. return row
  785. }
  786. }
  787. private func creativeDeepBackground(theme: NSColor) -> NSColor {
  788. let navy = NSColor(srgbRed: 0.08, green: 0.1, blue: 0.18, alpha: 1)
  789. let plum = NSColor(srgbRed: 0.14, green: 0.07, blue: 0.24, alpha: 1)
  790. switch idVariant % 4 {
  791. case 0: return theme.blended(withFraction: 0.52, of: navy) ?? theme
  792. case 1: return theme.blended(withFraction: 0.7, of: NSColor.black) ?? theme
  793. case 2: return palette.previewInk.blended(withFraction: 0.38, of: theme) ?? theme
  794. default: return theme.blended(withFraction: 0.4, of: plum) ?? theme
  795. }
  796. }
  797. private func creativeSidebarHeading(_ raw: String, onSidebar: NSColor, accent: NSColor) -> NSView {
  798. let t = makeLabel(raw, font: .systemFont(ofSize: 5.5, weight: .heavy), color: onSidebar, alignment: .left, maxLines: 1)
  799. let bar = NSView()
  800. bar.translatesAutoresizingMaskIntoConstraints = false
  801. bar.wantsLayer = true
  802. bar.layer?.backgroundColor = accent.cgColor
  803. bar.heightAnchor.constraint(equalToConstant: 2).isActive = true
  804. let c = NSStackView(views: [t, bar])
  805. c.orientation = .vertical
  806. c.spacing = 2
  807. c.alignment = .leading
  808. bar.leadingAnchor.constraint(equalTo: t.leadingAnchor).isActive = true
  809. bar.trailingAnchor.constraint(lessThanOrEqualTo: c.trailingAnchor).isActive = true
  810. bar.widthAnchor.constraint(equalToConstant: 56).isActive = true
  811. return c
  812. }
  813. private func creativeMainHeader(theme: NSColor) -> NSView {
  814. let v = NSView()
  815. v.translatesAutoresizingMaskIntoConstraints = false
  816. v.wantsLayer = true
  817. v.layer?.borderColor = theme.cgColor
  818. v.layer?.borderWidth = 0
  819. let stripe = NSView()
  820. stripe.translatesAutoresizingMaskIntoConstraints = false
  821. stripe.wantsLayer = true
  822. stripe.layer?.backgroundColor = theme.cgColor
  823. v.addSubview(stripe)
  824. let row = NSStackView()
  825. row.orientation = .horizontal
  826. row.spacing = 5
  827. row.translatesAutoresizingMaskIntoConstraints = false
  828. let lab = makeLabel("PORTFOLIO SNAPSHOT", font: .systemFont(ofSize: 6.5, weight: .black), color: palette.previewInk, alignment: .left, maxLines: 1)
  829. row.addArrangedSubview(stripe)
  830. row.addArrangedSubview(lab)
  831. v.addSubview(row)
  832. NSLayoutConstraint.activate([
  833. stripe.widthAnchor.constraint(equalToConstant: 3),
  834. stripe.heightAnchor.constraint(equalToConstant: 12),
  835. row.leadingAnchor.constraint(equalTo: v.leadingAnchor),
  836. row.topAnchor.constraint(equalTo: v.topAnchor),
  837. row.bottomAnchor.constraint(equalTo: v.bottomAnchor)
  838. ])
  839. return v
  840. }
  841. // MARK: - Shared pieces
  842. private func sectionHeading(_ raw: String) -> NSTextField {
  843. let s = formattedSectionLabel(raw)
  844. let accent = accentDecorationColor()
  845. return makeLabel(s, font: .systemFont(ofSize: 5.8, weight: .bold), color: accent, alignment: .left, maxLines: 1)
  846. }
  847. private func formattedSectionLabel(_ raw: String) -> String {
  848. switch template.sectionLabelStyle {
  849. case .uppercase: return raw
  850. case .slashed: return "// \(raw.capitalized)"
  851. case .bracketed: return "[ \(raw) ]"
  852. }
  853. }
  854. private func accentDecorationColor() -> NSColor {
  855. switch template.accent {
  856. case .redUnderline, .redBar: return palette.previewAccentRed
  857. case .blueBar: return palette.previewAccentBlue
  858. case .none: return template.themeColor.blended(withFraction: 0.5, of: palette.previewInk) ?? palette.previewInk
  859. }
  860. }
  861. private func headlineAccent(theme: NSColor, width: CGFloat) -> NSView {
  862. let v = NSView()
  863. v.translatesAutoresizingMaskIntoConstraints = false
  864. v.wantsLayer = true
  865. switch template.accent {
  866. case .none:
  867. v.layer?.backgroundColor = theme.withAlphaComponent(0.35).cgColor
  868. case .redUnderline:
  869. v.layer?.backgroundColor = palette.previewAccentRed.cgColor
  870. case .redBar:
  871. v.layer?.backgroundColor = palette.previewAccentRed.cgColor
  872. case .blueBar:
  873. v.layer?.backgroundColor = palette.previewAccentBlue.cgColor
  874. }
  875. v.heightAnchor.constraint(equalToConstant: template.accent == .redUnderline ? 1.2 : 2.2).isActive = true
  876. v.widthAnchor.constraint(equalToConstant: 72 * width).isActive = true
  877. return v
  878. }
  879. private func hairline() -> NSView {
  880. let v = NSView()
  881. v.translatesAutoresizingMaskIntoConstraints = false
  882. v.wantsLayer = true
  883. v.layer?.backgroundColor = palette.previewMuted.withAlphaComponent(0.35).cgColor
  884. v.heightAnchor.constraint(equalToConstant: 1).isActive = true
  885. return v
  886. }
  887. private func hairlineSoft() -> NSView {
  888. let v = NSView()
  889. v.translatesAutoresizingMaskIntoConstraints = false
  890. v.wantsLayer = true
  891. v.layer?.backgroundColor = palette.previewMuted.withAlphaComponent(0.22).cgColor
  892. v.heightAnchor.constraint(equalToConstant: 1).isActive = true
  893. return v
  894. }
  895. private func initialsAvatar(diameter: CGFloat, ink: NSColor) -> NSView {
  896. let avatar = NSView()
  897. avatar.translatesAutoresizingMaskIntoConstraints = false
  898. avatar.wantsLayer = true
  899. avatar.layer?.backgroundColor = template.themeColor.withAlphaComponent(0.12).cgColor
  900. avatar.layer?.borderColor = template.themeColor.withAlphaComponent(0.35).cgColor
  901. avatar.layer?.borderWidth = 1
  902. avatar.layer?.cornerRadius = diameter / 2
  903. avatar.widthAnchor.constraint(equalToConstant: diameter).isActive = true
  904. avatar.heightAnchor.constraint(equalToConstant: diameter).isActive = true
  905. let initials = makeLabel("SJ", font: .systemFont(ofSize: diameter * 0.32, weight: .bold), color: ink, alignment: .center, maxLines: 1)
  906. initials.translatesAutoresizingMaskIntoConstraints = false
  907. avatar.addSubview(initials)
  908. NSLayoutConstraint.activate([
  909. initials.centerXAnchor.constraint(equalTo: avatar.centerXAnchor),
  910. initials.centerYAnchor.constraint(equalTo: avatar.centerYAnchor)
  911. ])
  912. return avatar
  913. }
  914. private func skillLineBullet(_ text: String) -> NSView {
  915. makeLabel("· \(text)", font: .systemFont(ofSize: 6.1, weight: .regular), color: palette.previewInk, alignment: .left, maxLines: 1)
  916. }
  917. private func bulletRow(_ text: String, size: CGFloat) -> NSView {
  918. let row = NSStackView()
  919. row.orientation = .horizontal
  920. row.spacing = 3
  921. row.alignment = .top
  922. let mark = makeLabel("•", font: .systemFont(ofSize: size, weight: .bold), color: template.themeColor, alignment: .left, maxLines: 1)
  923. mark.setContentHuggingPriority(.required, for: .horizontal)
  924. let body = makeLabel(text, font: .systemFont(ofSize: size), color: palette.previewInk, alignment: .left, maxLines: 3)
  925. row.addArrangedSubview(mark)
  926. row.addArrangedSubview(body)
  927. return row
  928. }
  929. private func makeLabel(
  930. _ text: String,
  931. font: NSFont,
  932. color: NSColor,
  933. alignment: NSTextAlignment,
  934. maxLines: Int
  935. ) -> NSTextField {
  936. let tf: NSTextField
  937. if maxLines == 1 {
  938. tf = NSTextField(labelWithString: text)
  939. } else {
  940. tf = NSTextField(wrappingLabelWithString: text)
  941. tf.maximumNumberOfLines = maxLines
  942. }
  943. tf.font = font
  944. tf.textColor = color
  945. tf.alignment = alignment
  946. tf.isEditable = false
  947. tf.isSelectable = false
  948. tf.drawsBackground = false
  949. tf.isBordered = false
  950. tf.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
  951. return tf
  952. }
  953. }