설명 없음

CVTemplateMiniPreview.swift 51KB

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