Без опису

CVTemplateMiniPreview.swift 51KB

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