CVTemplateMiniPreview.swift 52 KB

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