Ei kuvausta

CVTemplateMiniPreview.swift 51KB

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