Ingen beskrivning

CVTemplateMiniPreview.swift 51KB

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