Nenhuma descrição

CVTemplateMiniPreview.swift 52KB

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