Bez popisu

CVTemplateMiniPreview.swift 51KB

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