|
@@ -0,0 +1,1041 @@
|
|
|
|
|
+//
|
|
|
|
|
+// CVTemplateMiniPreview.swift
|
|
|
|
|
+// App for Indeed
|
|
|
|
|
+//
|
|
|
|
|
+// Realistic mini résumé thumbnails for the template gallery — typography,
|
|
|
|
|
+// spacing, and layout vary by design family so cards read as finished previews.
|
|
|
|
|
+//
|
|
|
|
|
+
|
|
|
|
|
+import Cocoa
|
|
|
|
|
+
|
|
|
|
|
+// MARK: - Palette (shared with gallery card)
|
|
|
|
|
+
|
|
|
|
|
+struct CVTemplateCardPalette {
|
|
|
|
|
+ let border: NSColor
|
|
|
|
|
+ let borderHover: NSColor
|
|
|
|
|
+ let borderSelected: NSColor
|
|
|
|
|
+ let selectionGlow: NSColor
|
|
|
|
|
+ let footerBackground: NSColor
|
|
|
|
|
+ let previewSurface: NSColor
|
|
|
|
|
+ let previewPaper: NSColor
|
|
|
|
|
+ let previewSidebarTint: NSColor
|
|
|
|
|
+ let previewInk: NSColor
|
|
|
|
|
+ let previewMuted: NSColor
|
|
|
|
|
+ let previewAccentRed: NSColor
|
|
|
|
|
+ let previewAccentBlue: NSColor
|
|
|
|
|
+ let primaryText: NSColor
|
|
|
|
|
+ let secondaryText: NSColor
|
|
|
|
|
+ let overlayTint: NSColor
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// MARK: - Demo résumé content
|
|
|
|
|
+
|
|
|
|
|
+fileprivate enum CVPreviewDemoContent {
|
|
|
|
|
+ static let fullName = "Sarah Johnson"
|
|
|
|
|
+ static let title = "Senior Product Manager"
|
|
|
|
|
+ static let company = "Google"
|
|
|
|
|
+ static let companyLine = "Google · Mountain View, CA · 2019 – Present"
|
|
|
|
|
+ static let university = "Stanford University"
|
|
|
|
|
+ static let degree = "M.S. Management Science & Engineering"
|
|
|
|
|
+ static let email = "sarah.johnson@email.com"
|
|
|
|
|
+ static let phone = "(415) 555-0198"
|
|
|
|
|
+ static let location = "Mountain View, CA"
|
|
|
|
|
+ static let summary = "Product leader shipping roadmap, discovery, and analytics for high-scale consumer experiences."
|
|
|
|
|
+ static let bullet1 = "Defined multi-year platform strategy with exec stakeholders and quarterly OKRs."
|
|
|
|
|
+ static let bullet2 = "Partnered with engineering and design to launch experiments improving activation by 12%."
|
|
|
|
|
+ static let skillsList = ["Product Strategy", "SQL", "Figma", "A/B Testing", "Roadmapping"]
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// MARK: - Mini preview
|
|
|
|
|
+
|
|
|
|
|
+final class CVTemplatePreviewView: NSView {
|
|
|
|
|
+ private let template: CVTemplate
|
|
|
|
|
+ private let palette: CVTemplateCardPalette
|
|
|
|
|
+ private let paper = NSView()
|
|
|
|
|
+
|
|
|
|
|
+ /// Stable 0…11 from template identity so each row looks different (not just by `family`).
|
|
|
|
|
+ private var idVariant: Int {
|
|
|
|
|
+ var h: UInt64 = 1469598103934665603
|
|
|
|
|
+ let layoutDesc: String
|
|
|
|
|
+ switch template.layout {
|
|
|
|
|
+ case .singleColumn: layoutDesc = "1col"
|
|
|
|
|
+ case .twoColumn(let s, let t): layoutDesc = "2col_\(s)_\(t)"
|
|
|
|
|
+ }
|
|
|
|
|
+ let blob = "\(template.id)|\(template.family.rawValue)|\(template.headline)|\(template.accent)|\(layoutDesc)|\(template.sectionLabelStyle)"
|
|
|
|
|
+ for b in blob.utf8 {
|
|
|
|
|
+ h ^= UInt64(b)
|
|
|
|
|
+ h &*= 1_099_511_628_211
|
|
|
|
|
+ }
|
|
|
|
|
+ return Int(h % 12)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ init(template: CVTemplate, palette: CVTemplateCardPalette) {
|
|
|
|
|
+ self.template = template
|
|
|
|
|
+ self.palette = palette
|
|
|
|
|
+ super.init(frame: .zero)
|
|
|
|
|
+ wantsLayer = true
|
|
|
|
|
+ translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ configurePaper()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @available(*, unavailable)
|
|
|
|
|
+ required init?(coder: NSCoder) {
|
|
|
|
|
+ fatalError("init(coder:) has not been implemented")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func configurePaper() {
|
|
|
|
|
+ paper.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ paper.wantsLayer = true
|
|
|
|
|
+ paper.layer?.backgroundColor = paperBackgroundColor().cgColor
|
|
|
|
|
+ paper.layer?.cornerRadius = 6
|
|
|
|
|
+ paper.layer?.borderColor = NSColor(srgbRed: 228 / 255, green: 232 / 255, blue: 240 / 255, alpha: 1).cgColor
|
|
|
|
|
+ paper.layer?.borderWidth = 1
|
|
|
|
|
+ paper.layer?.masksToBounds = true
|
|
|
|
|
+ addSubview(paper)
|
|
|
|
|
+
|
|
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
|
|
+ paper.topAnchor.constraint(equalTo: topAnchor),
|
|
|
|
|
+ paper.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
|
|
|
+ paper.centerXAnchor.constraint(equalTo: centerXAnchor),
|
|
|
|
|
+ paper.widthAnchor.constraint(equalTo: heightAnchor, multiplier: 0.82),
|
|
|
|
|
+ paper.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor),
|
|
|
|
|
+ paper.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor)
|
|
|
|
|
+ ])
|
|
|
|
|
+
|
|
|
|
|
+ let root = buildResumeRoot()
|
|
|
|
|
+ root.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ paper.addSubview(root)
|
|
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
|
|
+ root.leadingAnchor.constraint(equalTo: paper.leadingAnchor, constant: 7),
|
|
|
|
|
+ root.trailingAnchor.constraint(equalTo: paper.trailingAnchor, constant: -7),
|
|
|
|
|
+ root.topAnchor.constraint(equalTo: paper.topAnchor, constant: 7),
|
|
|
|
|
+ root.bottomAnchor.constraint(lessThanOrEqualTo: paper.bottomAnchor, constant: -7)
|
|
|
|
|
+ ])
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func buildResumeRoot() -> NSView {
|
|
|
|
|
+ switch template.family {
|
|
|
|
|
+ case .professional: return buildProfessionalResume()
|
|
|
|
|
+ case .modern: return buildModernResume()
|
|
|
|
|
+ case .minimal: return buildMinimalResume()
|
|
|
|
|
+ case .executive: return buildExecutiveResume()
|
|
|
|
|
+ case .creative: return buildCreativeResume()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func paperBackgroundColor() -> NSColor {
|
|
|
|
|
+ switch idVariant % 5 {
|
|
|
|
|
+ case 0: return palette.previewPaper
|
|
|
|
|
+ case 1: return NSColor(srgbRed: 0.995, green: 0.992, blue: 0.985, alpha: 1)
|
|
|
|
|
+ case 2: return NSColor(srgbRed: 0.96, green: 0.99, blue: 1, alpha: 1)
|
|
|
|
|
+ case 3: return NSColor(srgbRed: 0.99, green: 0.99, blue: 0.99, alpha: 1)
|
|
|
|
|
+ default: return NSColor(srgbRed: 0.99, green: 0.98, blue: 0.995, alpha: 1)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // MARK: - Family: Professional (ATS-friendly)
|
|
|
|
|
+
|
|
|
|
|
+ private func buildProfessionalResume() -> NSView {
|
|
|
|
|
+ let swapExpFirst = (idVariant % 3) == 1
|
|
|
|
|
+ let showRail = (idVariant % 4) == 0
|
|
|
|
|
+ let sidebarMult: CGFloat = (idVariant % 5 == 2) ? 0.38 : 0.34
|
|
|
|
|
+
|
|
|
|
|
+ switch template.layout {
|
|
|
|
|
+ case .singleColumn:
|
|
|
|
|
+ let v = NSStackView()
|
|
|
|
|
+ v.orientation = .vertical
|
|
|
|
|
+ v.spacing = 4 + CGFloat(idVariant % 3)
|
|
|
|
|
+ v.alignment = .leading
|
|
|
|
|
+ v.addArrangedSubview(proHeaderBlock())
|
|
|
|
|
+ if (idVariant % 6) == 4 {
|
|
|
|
|
+ v.addArrangedSubview(proInlineSkillsRow())
|
|
|
|
|
+ }
|
|
|
|
|
+ v.addArrangedSubview(hairline())
|
|
|
|
|
+ let main = proMainColumn(compact: false, experienceFirst: swapExpFirst)
|
|
|
|
|
+ if showRail {
|
|
|
|
|
+ v.addArrangedSubview(horizontalWithLeadingRail(theme: template.themeColor, content: main))
|
|
|
|
|
+ } else {
|
|
|
|
|
+ v.addArrangedSubview(main)
|
|
|
|
|
+ }
|
|
|
|
|
+ return v
|
|
|
|
|
+ case .twoColumn(let side, let tinted):
|
|
|
|
|
+ let bar = proHeaderBlock()
|
|
|
|
|
+ let rule = hairline()
|
|
|
|
|
+ let row = NSStackView()
|
|
|
|
|
+ row.orientation = .horizontal
|
|
|
|
|
+ row.spacing = 5 + CGFloat(idVariant % 3)
|
|
|
|
|
+ row.alignment = .top
|
|
|
|
|
+ let sidebar = proSidebarColumn(tinted: tinted, variant: idVariant)
|
|
|
|
|
+ let main = proMainColumn(compact: true, experienceFirst: swapExpFirst)
|
|
|
|
|
+ if side == .leading {
|
|
|
|
|
+ row.addArrangedSubview(sidebar)
|
|
|
|
|
+ row.addArrangedSubview(main)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ row.addArrangedSubview(main)
|
|
|
|
|
+ row.addArrangedSubview(sidebar)
|
|
|
|
|
+ }
|
|
|
|
|
+ sidebar.widthAnchor.constraint(equalTo: row.widthAnchor, multiplier: sidebarMult).isActive = true
|
|
|
|
|
+
|
|
|
|
|
+ let wrap = NSStackView(views: [bar, rule, row])
|
|
|
|
|
+ wrap.orientation = .vertical
|
|
|
|
|
+ wrap.spacing = 5
|
|
|
|
|
+ wrap.alignment = .leading
|
|
|
|
|
+ return wrap
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func horizontalWithLeadingRail(theme: NSColor, content: NSView) -> NSView {
|
|
|
|
|
+ let rail = NSView()
|
|
|
|
|
+ rail.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ rail.wantsLayer = true
|
|
|
|
|
+ rail.layer?.backgroundColor = theme.cgColor
|
|
|
|
|
+ rail.layer?.cornerRadius = 1
|
|
|
|
|
+ rail.widthAnchor.constraint(equalToConstant: 3).isActive = true
|
|
|
|
|
+ let row = NSStackView(views: [rail, content])
|
|
|
|
|
+ row.orientation = .horizontal
|
|
|
|
|
+ row.spacing = 7
|
|
|
|
|
+ row.alignment = .top
|
|
|
|
|
+ return row
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func proInlineSkillsRow() -> NSView {
|
|
|
|
|
+ let joined = CVPreviewDemoContent.skillsList.prefix(4).joined(separator: " · ")
|
|
|
|
|
+ return makeLabel(joined, font: .systemFont(ofSize: 5.8, weight: .medium), color: template.themeColor, alignment: .left, maxLines: 2)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func proHeaderBlock() -> NSView {
|
|
|
|
|
+ let theme = template.themeColor
|
|
|
|
|
+ let ink = palette.previewInk
|
|
|
|
|
+ let muted = palette.previewMuted
|
|
|
|
|
+ let name = makeLabel(CVPreviewDemoContent.fullName, font: .systemFont(ofSize: 9.5, weight: .semibold), color: ink, alignment: .left, maxLines: 1)
|
|
|
|
|
+ let role = makeLabel(CVPreviewDemoContent.title, font: .systemFont(ofSize: 7.4, weight: .regular), color: muted, alignment: .left, maxLines: 1)
|
|
|
|
|
+ let contact = makeLabel("\(CVPreviewDemoContent.email) · \(CVPreviewDemoContent.phone)", font: .systemFont(ofSize: 6.2, weight: .regular), color: muted.withAlphaComponent(0.88), alignment: .left, maxLines: 1)
|
|
|
|
|
+
|
|
|
|
|
+ let textCol = NSStackView(views: [name, role, contact])
|
|
|
|
|
+ textCol.orientation = .vertical
|
|
|
|
|
+ textCol.spacing = 2
|
|
|
|
|
+ textCol.alignment = .leading
|
|
|
|
|
+
|
|
|
|
|
+ let row = NSStackView()
|
|
|
|
|
+ row.orientation = .horizontal
|
|
|
|
|
+ row.spacing = 6
|
|
|
|
|
+ row.alignment = .centerY
|
|
|
|
|
+
|
|
|
|
|
+ switch template.headline {
|
|
|
|
|
+ case .centered:
|
|
|
|
|
+ textCol.alignment = .centerX
|
|
|
|
|
+ name.alignment = .center
|
|
|
|
|
+ role.alignment = .center
|
|
|
|
|
+ contact.alignment = .center
|
|
|
|
|
+ let accent = headlineAccent(theme: theme, width: 0.42)
|
|
|
|
|
+ let stack = NSStackView(views: [textCol, accent])
|
|
|
|
|
+ stack.orientation = .vertical
|
|
|
|
|
+ stack.spacing = 4
|
|
|
|
|
+ stack.alignment = .centerX
|
|
|
|
|
+ return stack
|
|
|
|
|
+ case .leftAligned:
|
|
|
|
|
+ row.addArrangedSubview(textCol)
|
|
|
|
|
+ let col = NSStackView(views: [row, headlineAccent(theme: theme, width: 0.38)])
|
|
|
|
|
+ col.orientation = .vertical
|
|
|
|
|
+ col.spacing = 4
|
|
|
|
|
+ col.alignment = .leading
|
|
|
|
|
+ return col
|
|
|
|
|
+ case .leftWithInitials:
|
|
|
|
|
+ let avatar = initialsAvatar(diameter: 22, ink: ink)
|
|
|
|
|
+ row.addArrangedSubview(textCol)
|
|
|
|
|
+ row.addArrangedSubview(NSView()) // spacer
|
|
|
|
|
+ row.addArrangedSubview(avatar)
|
|
|
|
|
+ let col = NSStackView(views: [row, headlineAccent(theme: theme, width: 0.36)])
|
|
|
|
|
+ col.orientation = .vertical
|
|
|
|
|
+ col.spacing = 4
|
|
|
|
|
+ col.alignment = .leading
|
|
|
|
|
+ return col
|
|
|
|
|
+ case .avatarStacked:
|
|
|
|
|
+ let avatar = initialsAvatar(diameter: 24, ink: ink)
|
|
|
|
|
+ let stack = NSStackView()
|
|
|
|
|
+ stack.orientation = .vertical
|
|
|
|
|
+ stack.spacing = 3
|
|
|
|
|
+ stack.alignment = .centerX
|
|
|
|
|
+ stack.addArrangedSubview(avatar)
|
|
|
|
|
+ textCol.alignment = .centerX
|
|
|
|
|
+ name.alignment = .center
|
|
|
|
|
+ role.alignment = .center
|
|
|
|
|
+ contact.alignment = .center
|
|
|
|
|
+ stack.addArrangedSubview(textCol)
|
|
|
|
|
+ stack.addArrangedSubview(headlineAccent(theme: theme, width: 0.4))
|
|
|
|
|
+ return stack
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func proSidebarColumn(tinted: Bool, variant: Int) -> NSView {
|
|
|
|
|
+ let box = NSStackView()
|
|
|
|
|
+ box.orientation = .vertical
|
|
|
|
|
+ box.spacing = 4 + CGFloat(variant % 3)
|
|
|
|
|
+ box.alignment = .leading
|
|
|
|
|
+ if tinted {
|
|
|
|
|
+ box.wantsLayer = true
|
|
|
|
|
+ let tint = (variant % 4 == 1)
|
|
|
|
|
+ ? template.themeColor.withAlphaComponent(0.08)
|
|
|
|
|
+ : palette.previewSidebarTint
|
|
|
|
|
+ box.layer?.backgroundColor = tint.cgColor
|
|
|
|
|
+ box.layer?.cornerRadius = variant % 3 == 0 ? 5 : 4
|
|
|
|
|
+ }
|
|
|
|
|
+ let pad: CGFloat = tinted ? 5 : 0
|
|
|
|
|
+ let inner = NSStackView()
|
|
|
|
|
+ inner.edgeInsets = NSEdgeInsets(top: pad, left: pad, bottom: pad, right: pad)
|
|
|
|
|
+ inner.orientation = .vertical
|
|
|
|
|
+ inner.spacing = 5
|
|
|
|
|
+ inner.alignment = .leading
|
|
|
|
|
+ inner.addArrangedSubview(sectionHeading("CONTACT"))
|
|
|
|
|
+ inner.addArrangedSubview(makeLabel(CVPreviewDemoContent.email, font: .systemFont(ofSize: 6.2), color: palette.previewInk, alignment: .left, maxLines: 2))
|
|
|
|
|
+ inner.addArrangedSubview(makeLabel(CVPreviewDemoContent.phone, font: .systemFont(ofSize: 6.2), color: palette.previewMuted, alignment: .left, maxLines: 1))
|
|
|
|
|
+ inner.addArrangedSubview(makeLabel(CVPreviewDemoContent.location, font: .systemFont(ofSize: 6.2), color: palette.previewMuted, alignment: .left, maxLines: 1))
|
|
|
|
|
+ inner.addArrangedSubview(sectionHeading("SKILLS"))
|
|
|
|
|
+ if variant % 5 == 2 {
|
|
|
|
|
+ inner.addArrangedSubview(tagRow(theme: template.themeColor))
|
|
|
|
|
+ } else {
|
|
|
|
|
+ for s in CVPreviewDemoContent.skillsList.prefix(4) {
|
|
|
|
|
+ inner.addArrangedSubview(skillLineBullet(s))
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if variant % 7 == 3 {
|
|
|
|
|
+ inner.addArrangedSubview(sectionHeading("TOOLS"))
|
|
|
|
|
+ inner.addArrangedSubview(makeLabel("Amplitude · Jira · BigQuery", font: .systemFont(ofSize: 5.9), color: palette.previewMuted, alignment: .left, maxLines: 1))
|
|
|
|
|
+ }
|
|
|
|
|
+ box.addArrangedSubview(inner)
|
|
|
|
|
+ return box
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func proMainColumn(compact: Bool, experienceFirst: Bool) -> NSView {
|
|
|
|
|
+ let stack = NSStackView()
|
|
|
|
|
+ stack.orientation = .vertical
|
|
|
|
|
+ stack.spacing = compact ? 4 : 5 + CGFloat(idVariant % 2)
|
|
|
|
|
+ stack.alignment = .leading
|
|
|
|
|
+ let sp: CGFloat = compact ? 6.2 : 6.5
|
|
|
|
|
+
|
|
|
|
|
+ let profileBlock: () -> Void = {
|
|
|
|
|
+ stack.addArrangedSubview(self.sectionHeading("PROFILE"))
|
|
|
|
|
+ stack.addArrangedSubview(self.makeLabel(CVPreviewDemoContent.summary, font: .systemFont(ofSize: sp), color: self.palette.previewInk, alignment: .left, maxLines: 3))
|
|
|
|
|
+ }
|
|
|
|
|
+ let experienceBlock: () -> Void = {
|
|
|
|
|
+ stack.addArrangedSubview(self.sectionHeading("EXPERIENCE"))
|
|
|
|
|
+ stack.addArrangedSubview(self.makeLabel(CVPreviewDemoContent.title, font: .systemFont(ofSize: 6.8, weight: .semibold), color: self.palette.previewInk, alignment: .left, maxLines: 1))
|
|
|
|
|
+ stack.addArrangedSubview(self.makeLabel(CVPreviewDemoContent.companyLine, font: .systemFont(ofSize: 6.2, weight: .medium), color: self.template.themeColor, alignment: .left, maxLines: 1))
|
|
|
|
|
+ stack.addArrangedSubview(self.bulletRow(CVPreviewDemoContent.bullet1, size: sp))
|
|
|
|
|
+ stack.addArrangedSubview(self.bulletRow(CVPreviewDemoContent.bullet2, size: sp))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if experienceFirst {
|
|
|
|
|
+ experienceBlock()
|
|
|
|
|
+ profileBlock()
|
|
|
|
|
+ } else {
|
|
|
|
|
+ profileBlock()
|
|
|
|
|
+ experienceBlock()
|
|
|
|
|
+ }
|
|
|
|
|
+ stack.addArrangedSubview(sectionHeading("EDUCATION"))
|
|
|
|
|
+ stack.addArrangedSubview(makeLabel(CVPreviewDemoContent.university, font: .systemFont(ofSize: 6.6, weight: .semibold), color: palette.previewInk, alignment: .left, maxLines: 1))
|
|
|
|
|
+ stack.addArrangedSubview(makeLabel(CVPreviewDemoContent.degree, font: .systemFont(ofSize: 6.2), color: palette.previewMuted, alignment: .left, maxLines: 2))
|
|
|
|
|
+ return stack
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // MARK: - Family: Modern (three distinct silhouettes per id)
|
|
|
|
|
+
|
|
|
|
|
+ private func buildModernResume() -> NSView {
|
|
|
|
|
+ switch idVariant % 3 {
|
|
|
|
|
+ case 0: return buildModernClassicBandLayout()
|
|
|
|
|
+ case 1: return buildModernRailDocLayout()
|
|
|
|
|
+ default: return buildModernSplitHeaderLayout()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func modernPrimaryBody(theme: NSColor) -> NSView {
|
|
|
|
|
+ switch template.layout {
|
|
|
|
|
+ case .singleColumn:
|
|
|
|
|
+ return modernBodySingleColumn(theme: theme)
|
|
|
|
|
+ case .twoColumn(let side, let tinted):
|
|
|
|
|
+ let main = modernBodySingleColumn(theme: theme)
|
|
|
|
|
+ let sideCol = modernSidebar(theme: theme, tinted: tinted)
|
|
|
|
|
+ let row = NSStackView()
|
|
|
|
|
+ row.orientation = .horizontal
|
|
|
|
|
+ row.spacing = 5 + CGFloat(idVariant % 3)
|
|
|
|
|
+ row.alignment = .top
|
|
|
|
|
+ if side == .leading {
|
|
|
|
|
+ row.addArrangedSubview(sideCol)
|
|
|
|
|
+ row.addArrangedSubview(main)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ row.addArrangedSubview(main)
|
|
|
|
|
+ row.addArrangedSubview(sideCol)
|
|
|
|
|
+ }
|
|
|
|
|
+ let mult: CGFloat = (idVariant % 4 == 2) ? 0.36 : 0.32
|
|
|
|
|
+ sideCol.widthAnchor.constraint(equalTo: row.widthAnchor, multiplier: mult).isActive = true
|
|
|
|
|
+ return row
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func buildModernClassicBandLayout() -> NSView {
|
|
|
|
|
+ let theme = template.themeColor
|
|
|
|
|
+ let header = NSView()
|
|
|
|
|
+ header.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ header.wantsLayer = true
|
|
|
|
|
+ header.layer?.backgroundColor = theme.cgColor
|
|
|
|
|
+ header.layer?.cornerRadius = idVariant % 2 == 0 ? 5 : 3
|
|
|
|
|
+
|
|
|
|
|
+ let white = NSColor.white
|
|
|
|
|
+ let name = makeLabel(CVPreviewDemoContent.fullName, font: .systemFont(ofSize: 9, weight: .bold), color: white, alignment: .left, maxLines: 1)
|
|
|
|
|
+ let role = makeLabel(CVPreviewDemoContent.title, font: .systemFont(ofSize: 7.2, weight: .medium), color: white.withAlphaComponent(0.92), alignment: .left, maxLines: 1)
|
|
|
|
|
+ let hstack = NSStackView(views: [name, role])
|
|
|
|
|
+ hstack.orientation = .vertical
|
|
|
|
|
+ hstack.spacing = 2
|
|
|
|
|
+ hstack.alignment = .leading
|
|
|
|
|
+ hstack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+
|
|
|
|
|
+ let iconRow = NSStackView()
|
|
|
|
|
+ iconRow.orientation = .horizontal
|
|
|
|
|
+ iconRow.spacing = 5
|
|
|
|
|
+ iconRow.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ for sym in ["mappin.and.ellipse", "phone.fill", "envelope.fill"] {
|
|
|
|
|
+ guard let img = NSImage(systemSymbolName: sym, accessibilityDescription: nil) else { continue }
|
|
|
|
|
+ let iv = NSImageView(image: img)
|
|
|
|
|
+ iv.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 6, weight: .semibold)
|
|
|
|
|
+ iv.contentTintColor = white.withAlphaComponent(0.85)
|
|
|
|
|
+ iconRow.addArrangedSubview(iv)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let topRow = NSStackView()
|
|
|
|
|
+ topRow.orientation = .horizontal
|
|
|
|
|
+ topRow.spacing = 8
|
|
|
|
|
+ topRow.alignment = .centerY
|
|
|
|
|
+ topRow.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ topRow.addArrangedSubview(hstack)
|
|
|
|
|
+ topRow.addArrangedSubview(NSView())
|
|
|
|
|
+ topRow.addArrangedSubview(iconRow)
|
|
|
|
|
+
|
|
|
|
|
+ header.addSubview(topRow)
|
|
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
|
|
+ topRow.leadingAnchor.constraint(equalTo: header.leadingAnchor, constant: 7),
|
|
|
|
|
+ topRow.trailingAnchor.constraint(equalTo: header.trailingAnchor, constant: -7),
|
|
|
|
|
+ topRow.topAnchor.constraint(equalTo: header.topAnchor, constant: 6),
|
|
|
|
|
+ topRow.bottomAnchor.constraint(equalTo: header.bottomAnchor, constant: -6),
|
|
|
|
|
+ header.heightAnchor.constraint(greaterThanOrEqualToConstant: 32 + CGFloat(idVariant % 3) * 2)
|
|
|
|
|
+ ])
|
|
|
|
|
+
|
|
|
|
|
+ let body = modernPrimaryBody(theme: theme)
|
|
|
|
|
+ let wrap = NSStackView(views: [header, body])
|
|
|
|
|
+ wrap.orientation = .vertical
|
|
|
|
|
+ wrap.spacing = 6 + CGFloat(idVariant % 2)
|
|
|
|
|
+ wrap.alignment = .leading
|
|
|
|
|
+ return wrap
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func buildModernRailDocLayout() -> NSView {
|
|
|
|
|
+ let theme = template.themeColor
|
|
|
|
|
+ let ink = palette.previewInk
|
|
|
|
|
+ let muted = palette.previewMuted
|
|
|
|
|
+ let rail = NSView()
|
|
|
|
|
+ rail.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ rail.wantsLayer = true
|
|
|
|
|
+ rail.layer?.backgroundColor = theme.cgColor
|
|
|
|
|
+ rail.layer?.cornerRadius = 2
|
|
|
|
|
+ rail.widthAnchor.constraint(equalToConstant: 3 + CGFloat(idVariant % 2)).isActive = true
|
|
|
|
|
+
|
|
|
|
|
+ let inner = NSStackView()
|
|
|
|
|
+ inner.orientation = .vertical
|
|
|
|
|
+ inner.spacing = 5
|
|
|
|
|
+ inner.alignment = .leading
|
|
|
|
|
+ inner.addArrangedSubview(makeLabel(CVPreviewDemoContent.fullName, font: .systemFont(ofSize: 9.5, weight: .bold), color: ink, alignment: .left, maxLines: 1))
|
|
|
|
|
+ inner.addArrangedSubview(makeLabel(CVPreviewDemoContent.title, font: .systemFont(ofSize: 7.2, weight: .semibold), color: theme, alignment: .left, maxLines: 1))
|
|
|
|
|
+ inner.addArrangedSubview(makeLabel("\(CVPreviewDemoContent.email) · \(CVPreviewDemoContent.phone)", font: .systemFont(ofSize: 6), color: muted, alignment: .left, maxLines: 1))
|
|
|
|
|
+ inner.addArrangedSubview(hairline())
|
|
|
|
|
+ inner.addArrangedSubview(tagRow(theme: theme))
|
|
|
|
|
+ inner.addArrangedSubview(modernPrimaryBody(theme: theme))
|
|
|
|
|
+
|
|
|
|
|
+ let row = NSStackView(views: [rail, inner])
|
|
|
|
|
+ row.orientation = .horizontal
|
|
|
|
|
+ row.spacing = 7
|
|
|
|
|
+ row.alignment = .top
|
|
|
|
|
+ return row
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func buildModernSplitHeaderLayout() -> NSView {
|
|
|
|
|
+ let theme = template.themeColor
|
|
|
|
|
+ let ink = palette.previewInk
|
|
|
|
|
+ let muted = palette.previewMuted
|
|
|
|
|
+ let left = NSStackView()
|
|
|
|
|
+ left.orientation = .vertical
|
|
|
|
|
+ left.spacing = 3
|
|
|
|
|
+ left.alignment = .leading
|
|
|
|
|
+ left.addArrangedSubview(makeLabel(CVPreviewDemoContent.fullName, font: .systemFont(ofSize: 9.2, weight: .bold), color: ink, alignment: .left, maxLines: 2))
|
|
|
|
|
+ left.addArrangedSubview(makeLabel(CVPreviewDemoContent.title, font: .systemFont(ofSize: 7, weight: .medium), color: muted, alignment: .left, maxLines: 2))
|
|
|
|
|
+ left.addArrangedSubview(makeLabel(CVPreviewDemoContent.location, font: .systemFont(ofSize: 6), color: muted.withAlphaComponent(0.85), alignment: .left, maxLines: 1))
|
|
|
|
|
+
|
|
|
|
|
+ let right = NSStackView()
|
|
|
|
|
+ right.orientation = .vertical
|
|
|
|
|
+ right.spacing = 4
|
|
|
|
|
+ right.alignment = .leading
|
|
|
|
|
+ right.wantsLayer = true
|
|
|
|
|
+ right.layer?.backgroundColor = theme.cgColor
|
|
|
|
|
+ right.layer?.cornerRadius = 5
|
|
|
|
|
+ right.edgeInsets = NSEdgeInsets(top: 6, left: 7, bottom: 6, right: 7)
|
|
|
|
|
+ let onW = NSColor.white
|
|
|
|
|
+ right.addArrangedSubview(makeLabel(CVPreviewDemoContent.email, font: .systemFont(ofSize: 5.9, weight: .medium), color: onW.withAlphaComponent(0.95), alignment: .left, maxLines: 2))
|
|
|
|
|
+ right.addArrangedSubview(makeLabel(CVPreviewDemoContent.phone, font: .systemFont(ofSize: 5.9, weight: .medium), color: onW.withAlphaComponent(0.9), alignment: .left, maxLines: 1))
|
|
|
|
|
+ right.addArrangedSubview(makeLabel("Open to relocation", font: .systemFont(ofSize: 5.6, weight: .regular), color: onW.withAlphaComponent(0.75), alignment: .left, maxLines: 1))
|
|
|
|
|
+
|
|
|
|
|
+ let top = NSStackView(views: [left, right])
|
|
|
|
|
+ top.orientation = .horizontal
|
|
|
|
|
+ top.spacing = 6
|
|
|
|
|
+ top.alignment = .top
|
|
|
|
|
+ left.widthAnchor.constraint(equalTo: top.widthAnchor, multiplier: 0.54).isActive = true
|
|
|
|
|
+
|
|
|
|
|
+ let col = NSStackView(views: [top, hairline(), modernPrimaryBody(theme: theme)])
|
|
|
|
|
+ col.orientation = .vertical
|
|
|
|
|
+ col.spacing = 7
|
|
|
|
|
+ col.alignment = .leading
|
|
|
|
|
+ return col
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func modernSidebar(theme: NSColor, tinted: Bool) -> NSView {
|
|
|
|
|
+ let box = NSStackView()
|
|
|
|
|
+ box.orientation = .vertical
|
|
|
|
|
+ box.spacing = 5
|
|
|
|
|
+ box.alignment = .leading
|
|
|
|
|
+ if tinted {
|
|
|
|
|
+ box.wantsLayer = true
|
|
|
|
|
+ box.layer?.backgroundColor = theme.withAlphaComponent(0.1).cgColor
|
|
|
|
|
+ box.layer?.cornerRadius = 4
|
|
|
|
|
+ }
|
|
|
|
|
+ box.edgeInsets = NSEdgeInsets(top: 4, left: 4, bottom: 4, right: 4)
|
|
|
|
|
+ box.addArrangedSubview(modernSectionRow(symbol: "person.crop.circle", title: "About", theme: theme))
|
|
|
|
|
+ box.addArrangedSubview(makeLabel(CVPreviewDemoContent.summary, font: .systemFont(ofSize: 6), color: palette.previewInk, alignment: .left, maxLines: 4))
|
|
|
|
|
+ box.addArrangedSubview(modernSectionRow(symbol: "star.fill", title: "Highlights", theme: theme))
|
|
|
|
|
+ box.addArrangedSubview(makeLabel("Launched growth experiments, SQL deep dives, design QA.", font: .systemFont(ofSize: 6), color: palette.previewMuted, alignment: .left, maxLines: 3))
|
|
|
|
|
+ return box
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func modernSectionRow(symbol: String, title: String, theme: NSColor) -> NSView {
|
|
|
|
|
+ guard let img = NSImage(systemSymbolName: symbol, accessibilityDescription: nil) else {
|
|
|
|
|
+ return makeLabel(title, font: .systemFont(ofSize: 6.5, weight: .bold), color: palette.previewInk, alignment: .left, maxLines: 1)
|
|
|
|
|
+ }
|
|
|
|
|
+ let iv = NSImageView(image: img)
|
|
|
|
|
+ iv.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 6, weight: .semibold)
|
|
|
|
|
+ iv.contentTintColor = theme
|
|
|
|
|
+ let t = makeLabel(title, font: .systemFont(ofSize: 6.5, weight: .bold), color: palette.previewInk, alignment: .left, maxLines: 1)
|
|
|
|
|
+ let r = NSStackView(views: [iv, t])
|
|
|
|
|
+ r.orientation = .horizontal
|
|
|
|
|
+ r.spacing = 4
|
|
|
|
|
+ r.alignment = .centerY
|
|
|
|
|
+ return r
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func modernBodySingleColumn(theme: NSColor) -> NSView {
|
|
|
|
|
+ let stack = NSStackView()
|
|
|
|
|
+ stack.orientation = .vertical
|
|
|
|
|
+ stack.spacing = 5
|
|
|
|
|
+ stack.alignment = .leading
|
|
|
|
|
+ stack.addArrangedSubview(modernSectionRow(symbol: "briefcase.fill", title: "Experience", theme: theme))
|
|
|
|
|
+ stack.addArrangedSubview(makeLabel("\(CVPreviewDemoContent.title) — \(CVPreviewDemoContent.company)", font: .systemFont(ofSize: 6.6, weight: .semibold), color: palette.previewInk, alignment: .left, maxLines: 2))
|
|
|
|
|
+ stack.addArrangedSubview(makeLabel("2019 – Present · Led cross-functional pods from discovery to launch.", font: .systemFont(ofSize: 6.1), color: palette.previewMuted, alignment: .left, maxLines: 2))
|
|
|
|
|
+ stack.addArrangedSubview(tagRow(theme: theme))
|
|
|
|
|
+ stack.addArrangedSubview(modernSectionRow(symbol: "graduationcap.fill", title: "Education", theme: theme))
|
|
|
|
|
+ stack.addArrangedSubview(makeLabel("\(CVPreviewDemoContent.university), \(CVPreviewDemoContent.degree)", font: .systemFont(ofSize: 6.2), color: palette.previewInk, alignment: .left, maxLines: 2))
|
|
|
|
|
+ return stack
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func tagRow(theme: NSColor) -> NSView {
|
|
|
|
|
+ let row = NSStackView()
|
|
|
|
|
+ row.orientation = .horizontal
|
|
|
|
|
+ row.spacing = 3
|
|
|
|
|
+ row.alignment = .centerY
|
|
|
|
|
+ for s in CVPreviewDemoContent.skillsList.prefix(4) {
|
|
|
|
|
+ let tag = NSView()
|
|
|
|
|
+ tag.wantsLayer = true
|
|
|
|
|
+ tag.layer?.backgroundColor = theme.withAlphaComponent(0.14).cgColor
|
|
|
|
|
+ tag.layer?.cornerRadius = 3
|
|
|
|
|
+ tag.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ 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)
|
|
|
|
|
+ lab.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ tag.addSubview(lab)
|
|
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
|
|
+ lab.leadingAnchor.constraint(equalTo: tag.leadingAnchor, constant: 4),
|
|
|
|
|
+ lab.trailingAnchor.constraint(equalTo: tag.trailingAnchor, constant: -4),
|
|
|
|
|
+ lab.topAnchor.constraint(equalTo: tag.topAnchor, constant: 2),
|
|
|
|
|
+ lab.bottomAnchor.constraint(equalTo: tag.bottomAnchor, constant: -2)
|
|
|
|
|
+ ])
|
|
|
|
|
+ row.addArrangedSubview(tag)
|
|
|
|
|
+ }
|
|
|
|
|
+ return row
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // MARK: - Family: Minimal
|
|
|
|
|
+
|
|
|
|
|
+ private func buildMinimalResume() -> NSView {
|
|
|
|
|
+ let ink = palette.previewInk
|
|
|
|
|
+ let muted = palette.previewMuted
|
|
|
|
|
+ let nameWeight: NSFont.Weight = (idVariant % 3 == 0) ? .ultraLight : .light
|
|
|
|
|
+ let nameSize: CGFloat = template.headline == .centered ? 11 + CGFloat(idVariant % 2) : 9.5 + CGFloat(idVariant % 3)
|
|
|
|
|
+ let name = makeLabel(CVPreviewDemoContent.fullName, font: .systemFont(ofSize: nameSize, weight: nameWeight), color: ink, alignment: .left, maxLines: 1)
|
|
|
|
|
+ let role = makeLabel(CVPreviewDemoContent.title.uppercased(), font: .systemFont(ofSize: 6.5, weight: .medium), color: muted, alignment: .left, maxLines: 1)
|
|
|
|
|
+ let contact = makeLabel("\(CVPreviewDemoContent.email) \(CVPreviewDemoContent.phone)", font: .systemFont(ofSize: 5.8, weight: .regular), color: muted.withAlphaComponent(0.75), alignment: .left, maxLines: 1)
|
|
|
|
|
+
|
|
|
|
|
+ let head = NSStackView()
|
|
|
|
|
+ head.orientation = .vertical
|
|
|
|
|
+ head.spacing = template.headline == .avatarStacked ? 8 : 4 + CGFloat(idVariant % 4)
|
|
|
|
|
+ head.alignment = template.headline == .centered ? .centerX : .leading
|
|
|
|
|
+ if template.headline == .avatarStacked {
|
|
|
|
|
+ head.addArrangedSubview(initialsAvatar(diameter: 24 + CGFloat(idVariant % 2) * 2, ink: ink))
|
|
|
|
|
+ }
|
|
|
|
|
+ if template.headline == .centered {
|
|
|
|
|
+ name.alignment = .center
|
|
|
|
|
+ role.alignment = .center
|
|
|
|
|
+ contact.alignment = .center
|
|
|
|
|
+ }
|
|
|
|
|
+ head.addArrangedSubview(name)
|
|
|
|
|
+ head.addArrangedSubview(role)
|
|
|
|
|
+ head.addArrangedSubview(contact)
|
|
|
|
|
+ head.addArrangedSubview(hairlineSoft())
|
|
|
|
|
+ if idVariant % 5 == 1 {
|
|
|
|
|
+ head.addArrangedSubview(hairlineSoft())
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let swapEdu = (idVariant % 4) == 2
|
|
|
|
|
+ let body: NSView
|
|
|
|
|
+ switch template.layout {
|
|
|
|
|
+ case .singleColumn:
|
|
|
|
|
+ body = minimalBody(spacing: 6 + CGFloat(idVariant % 3), educationBeforeExperience: swapEdu)
|
|
|
|
|
+ case .twoColumn(let side, _):
|
|
|
|
|
+ let a = minimalBody(spacing: 5, educationBeforeExperience: swapEdu)
|
|
|
|
|
+ let b = minimalAside(numbered: idVariant % 3 == 1)
|
|
|
|
|
+ let row = NSStackView()
|
|
|
|
|
+ row.orientation = .horizontal
|
|
|
|
|
+ row.spacing = 8 + CGFloat(idVariant % 3)
|
|
|
|
|
+ row.alignment = .top
|
|
|
|
|
+ if side == .leading {
|
|
|
|
|
+ row.addArrangedSubview(b)
|
|
|
|
|
+ row.addArrangedSubview(a)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ row.addArrangedSubview(a)
|
|
|
|
|
+ row.addArrangedSubview(b)
|
|
|
|
|
+ }
|
|
|
|
|
+ let mult: CGFloat = (idVariant % 5 == 0) ? 0.34 : 0.3
|
|
|
|
|
+ b.widthAnchor.constraint(equalTo: row.widthAnchor, multiplier: mult).isActive = true
|
|
|
|
|
+ body = row
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let wrap = NSStackView(views: [head, body])
|
|
|
|
|
+ wrap.orientation = .vertical
|
|
|
|
|
+ wrap.spacing = 7 + CGFloat(idVariant % 2)
|
|
|
|
|
+ wrap.alignment = .leading
|
|
|
|
|
+ return wrap
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func minimalBody(spacing: CGFloat, educationBeforeExperience: Bool) -> NSView {
|
|
|
|
|
+ let stack = NSStackView()
|
|
|
|
|
+ stack.orientation = .vertical
|
|
|
|
|
+ stack.spacing = spacing
|
|
|
|
|
+ stack.alignment = .leading
|
|
|
|
|
+
|
|
|
|
|
+ let edu: () -> Void = {
|
|
|
|
|
+ stack.addArrangedSubview(self.sectionHeading("EDUCATION"))
|
|
|
|
|
+ stack.addArrangedSubview(self.makeLabel("\(CVPreviewDemoContent.university) — \(CVPreviewDemoContent.degree)", font: .systemFont(ofSize: 6.1, weight: .light), color: self.palette.previewInk, alignment: .left, maxLines: 2))
|
|
|
|
|
+ }
|
|
|
|
|
+ let prof: () -> Void = {
|
|
|
|
|
+ stack.addArrangedSubview(self.sectionHeading("PROFILE"))
|
|
|
|
|
+ stack.addArrangedSubview(self.makeLabel(CVPreviewDemoContent.summary, font: .systemFont(ofSize: 6.3, weight: .light), color: self.palette.previewInk, alignment: .left, maxLines: 3))
|
|
|
|
|
+ }
|
|
|
|
|
+ let exp: () -> Void = {
|
|
|
|
|
+ stack.addArrangedSubview(self.sectionHeading("EXPERIENCE"))
|
|
|
|
|
+ stack.addArrangedSubview(self.makeLabel("\(CVPreviewDemoContent.title), \(CVPreviewDemoContent.company)", font: .systemFont(ofSize: 6.5, weight: .regular), color: self.palette.previewInk, alignment: .left, maxLines: 2))
|
|
|
|
|
+ stack.addArrangedSubview(self.makeLabel(CVPreviewDemoContent.bullet1, font: .systemFont(ofSize: 6, weight: .light), color: self.palette.previewMuted, alignment: .left, maxLines: 2))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if educationBeforeExperience {
|
|
|
|
|
+ edu()
|
|
|
|
|
+ prof()
|
|
|
|
|
+ exp()
|
|
|
|
|
+ } else {
|
|
|
|
|
+ prof()
|
|
|
|
|
+ exp()
|
|
|
|
|
+ edu()
|
|
|
|
|
+ }
|
|
|
|
|
+ return stack
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func minimalAside(numbered: Bool) -> NSView {
|
|
|
|
|
+ let stack = NSStackView()
|
|
|
|
|
+ stack.orientation = .vertical
|
|
|
|
|
+ stack.spacing = 6
|
|
|
|
|
+ stack.alignment = .leading
|
|
|
|
|
+ stack.addArrangedSubview(sectionHeading("SKILLS"))
|
|
|
|
|
+ for (i, s) in CVPreviewDemoContent.skillsList.enumerated() {
|
|
|
|
|
+ let prefix = numbered ? "\(i + 1). " : "· "
|
|
|
|
|
+ stack.addArrangedSubview(makeLabel("\(prefix)\(s)", font: .systemFont(ofSize: 6, weight: .light), color: palette.previewMuted, alignment: .left, maxLines: 1))
|
|
|
|
|
+ }
|
|
|
|
|
+ return stack
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // MARK: - Family: Executive (serif)
|
|
|
|
|
+
|
|
|
|
|
+ private func buildExecutiveResume() -> NSView {
|
|
|
|
|
+ let serif = NSFont(name: "Georgia", size: 6.4) ?? .systemFont(ofSize: 6.4)
|
|
|
|
|
+ let serifBase = NSFont(name: "Georgia", size: 7.8) ?? .systemFont(ofSize: 7.8)
|
|
|
|
|
+ let serifBold = NSFontManager.shared.convert(serifBase, toHaveTrait: .boldFontMask)
|
|
|
|
|
+ let ink = palette.previewInk
|
|
|
|
|
+ let muted = palette.previewMuted
|
|
|
|
|
+ let theme = template.themeColor
|
|
|
|
|
+ let centeredHead = (idVariant % 2) == 0
|
|
|
|
|
+
|
|
|
|
|
+ let name = makeLabel(CVPreviewDemoContent.fullName, font: serifBold, color: ink, alignment: centeredHead ? .center : .left, maxLines: 1)
|
|
|
|
|
+ let role = makeLabel(CVPreviewDemoContent.title, font: serif, color: muted, alignment: centeredHead ? .center : .left, maxLines: 1)
|
|
|
|
|
+ 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)
|
|
|
|
|
+
|
|
|
|
|
+ let rule = executiveRule(theme: theme, wide: idVariant % 3 == 0)
|
|
|
|
|
+ let head = NSStackView(views: [name, role, contact, rule])
|
|
|
|
|
+ head.orientation = .vertical
|
|
|
|
|
+ head.spacing = 4
|
|
|
|
|
+ head.alignment = centeredHead ? .centerX : .leading
|
|
|
|
|
+
|
|
|
|
|
+ let body: NSView
|
|
|
|
|
+ switch template.layout {
|
|
|
|
|
+ case .singleColumn:
|
|
|
|
|
+ body = executiveBody(serif: serif, ink: ink, muted: muted, theme: theme, compact: false, tightLeading: idVariant % 5 == 2)
|
|
|
|
|
+ case .twoColumn(let side, let tinted):
|
|
|
|
|
+ let main = executiveBody(serif: serif, ink: ink, muted: muted, theme: theme, compact: true, tightLeading: idVariant % 5 == 2)
|
|
|
|
|
+ let sideC = executiveSidebar(serif: serif, tinted: tinted, showMetrics: idVariant % 4 == 1)
|
|
|
|
|
+ let row = NSStackView()
|
|
|
|
|
+ row.orientation = .horizontal
|
|
|
|
|
+ row.spacing = 6 + CGFloat(idVariant % 3)
|
|
|
|
|
+ row.alignment = .top
|
|
|
|
|
+ if side == .leading {
|
|
|
|
|
+ row.addArrangedSubview(sideC)
|
|
|
|
|
+ row.addArrangedSubview(main)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ row.addArrangedSubview(main)
|
|
|
|
|
+ row.addArrangedSubview(sideC)
|
|
|
|
|
+ }
|
|
|
|
|
+ let mult: CGFloat = (idVariant % 5 == 3) ? 0.38 : 0.33
|
|
|
|
|
+ sideC.widthAnchor.constraint(equalTo: row.widthAnchor, multiplier: mult).isActive = true
|
|
|
|
|
+ body = row
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let wrap = NSStackView(views: [head, body])
|
|
|
|
|
+ wrap.orientation = .vertical
|
|
|
|
|
+ wrap.spacing = 6 + CGFloat(idVariant % 2)
|
|
|
|
|
+ wrap.alignment = .leading
|
|
|
|
|
+ return wrap
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func executiveRule(theme: NSColor, wide: Bool) -> NSView {
|
|
|
|
|
+ let v = NSView()
|
|
|
|
|
+ v.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ v.wantsLayer = true
|
|
|
|
|
+ v.layer?.backgroundColor = theme.withAlphaComponent(0.45).cgColor
|
|
|
|
|
+ v.heightAnchor.constraint(equalToConstant: wide ? 2 : 1).isActive = true
|
|
|
|
|
+ v.widthAnchor.constraint(equalToConstant: wide ? 160 : 110).isActive = true
|
|
|
|
|
+ return v
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func executiveBody(serif: NSFont, ink: NSColor, muted: NSColor, theme: NSColor, compact: Bool, tightLeading: Bool) -> NSView {
|
|
|
|
|
+ let stack = NSStackView()
|
|
|
|
|
+ stack.orientation = .vertical
|
|
|
|
|
+ stack.spacing = (compact ? 4 : 5) - (tightLeading ? 1 : 0)
|
|
|
|
|
+ stack.alignment = .leading
|
|
|
|
|
+ let sumTitle = (idVariant % 6 == 3) ? "SUMMARY" : "PROFESSIONAL SUMMARY"
|
|
|
|
|
+ stack.addArrangedSubview(sectionHeading(sumTitle))
|
|
|
|
|
+ stack.addArrangedSubview(makeLabel(CVPreviewDemoContent.summary, font: serif, color: ink, alignment: .left, maxLines: 3))
|
|
|
|
|
+ stack.addArrangedSubview(sectionHeading("SELECTED EXPERIENCE"))
|
|
|
|
|
+ let jobFont = NSFontManager.shared.convert(serif, toHaveTrait: .boldFontMask)
|
|
|
|
|
+ stack.addArrangedSubview(makeLabel("\(CVPreviewDemoContent.title), \(CVPreviewDemoContent.company)", font: jobFont, color: ink, alignment: .left, maxLines: 2))
|
|
|
|
|
+ stack.addArrangedSubview(makeLabel("2019 – Present", font: serif, color: theme, alignment: .left, maxLines: 1))
|
|
|
|
|
+ stack.addArrangedSubview(makeLabel(CVPreviewDemoContent.bullet1, font: serif, color: muted, alignment: .left, maxLines: 2))
|
|
|
|
|
+ stack.addArrangedSubview(sectionHeading("EDUCATION"))
|
|
|
|
|
+ stack.addArrangedSubview(makeLabel("\(CVPreviewDemoContent.university), \(CVPreviewDemoContent.degree)", font: serif, color: ink, alignment: .left, maxLines: 2))
|
|
|
|
|
+ return stack
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func executiveSidebar(serif: NSFont, tinted: Bool, showMetrics: Bool) -> NSView {
|
|
|
|
|
+ let stack = NSStackView()
|
|
|
|
|
+ stack.orientation = .vertical
|
|
|
|
|
+ stack.spacing = 4
|
|
|
|
|
+ stack.alignment = .leading
|
|
|
|
|
+ if tinted {
|
|
|
|
|
+ stack.wantsLayer = true
|
|
|
|
|
+ let fill = (idVariant % 3 == 0)
|
|
|
|
|
+ ? NSColor(srgbRed: 0.97, green: 0.97, blue: 0.98, alpha: 1)
|
|
|
|
|
+ : template.themeColor.withAlphaComponent(0.07)
|
|
|
|
|
+ stack.layer?.backgroundColor = fill.cgColor
|
|
|
|
|
+ stack.layer?.cornerRadius = idVariant % 4 == 2 ? 5 : 3
|
|
|
|
|
+ stack.edgeInsets = NSEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
|
|
|
|
|
+ }
|
|
|
|
|
+ stack.addArrangedSubview(sectionHeading("CORE COMPETENCIES"))
|
|
|
|
|
+ for s in CVPreviewDemoContent.skillsList {
|
|
|
|
|
+ stack.addArrangedSubview(makeLabel("· \(s)", font: serif, color: palette.previewInk, alignment: .left, maxLines: 1))
|
|
|
|
|
+ }
|
|
|
|
|
+ stack.addArrangedSubview(sectionHeading("TOOLS"))
|
|
|
|
|
+ stack.addArrangedSubview(makeLabel("Figma, SQL, Amplitude, Jira", font: serif, color: palette.previewMuted, alignment: .left, maxLines: 2))
|
|
|
|
|
+ if showMetrics {
|
|
|
|
|
+ stack.addArrangedSubview(sectionHeading("IMPACT"))
|
|
|
|
|
+ stack.addArrangedSubview(makeLabel("+12% activation · $4.2M ARR influenced", font: serif, color: palette.previewInk, alignment: .left, maxLines: 2))
|
|
|
|
|
+ }
|
|
|
|
|
+ return stack
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // MARK: - Family: Creative (sidebar + bold accent)
|
|
|
|
|
+
|
|
|
|
|
+ private func buildCreativeResume() -> NSView {
|
|
|
|
|
+ let theme = template.themeColor
|
|
|
|
|
+ let deep = creativeDeepBackground(theme: theme)
|
|
|
|
|
+ let onSidebar = NSColor.white.withAlphaComponent(0.95)
|
|
|
|
|
+ let skillPrefix = (idVariant % 3 == 0) ? "• " : "▸ "
|
|
|
|
|
+
|
|
|
|
|
+ let sidebar = NSStackView()
|
|
|
|
|
+ sidebar.orientation = .vertical
|
|
|
|
|
+ sidebar.spacing = 4 + CGFloat(idVariant % 3)
|
|
|
|
|
+ sidebar.alignment = .leading
|
|
|
|
|
+ sidebar.wantsLayer = true
|
|
|
|
|
+ sidebar.layer?.backgroundColor = deep.cgColor
|
|
|
|
|
+ sidebar.layer?.cornerRadius = idVariant % 2 == 0 ? 6 : 4
|
|
|
|
|
+ sidebar.edgeInsets = NSEdgeInsets(top: 6, left: 6, bottom: 6, right: 6)
|
|
|
|
|
+
|
|
|
|
|
+ let sbTitle = makeLabel(CVPreviewDemoContent.fullName, font: .systemFont(ofSize: 7.5 + CGFloat(idVariant % 2), weight: .bold), color: onSidebar, alignment: .left, maxLines: 2)
|
|
|
|
|
+ sidebar.addArrangedSubview(sbTitle)
|
|
|
|
|
+ sidebar.addArrangedSubview(makeLabel(CVPreviewDemoContent.title, font: .systemFont(ofSize: 6.5, weight: .medium), color: onSidebar.withAlphaComponent(0.85), alignment: .left, maxLines: 2))
|
|
|
|
|
+ sidebar.addArrangedSubview(makeLabel(CVPreviewDemoContent.email, font: .systemFont(ofSize: 5.8), color: onSidebar.withAlphaComponent(0.8), alignment: .left, maxLines: 1))
|
|
|
|
|
+ sidebar.addArrangedSubview(makeLabel(CVPreviewDemoContent.phone, font: .systemFont(ofSize: 5.8), color: onSidebar.withAlphaComponent(0.8), alignment: .left, maxLines: 1))
|
|
|
|
|
+ sidebar.addArrangedSubview(creativeSidebarHeading("STRENGTHS", onSidebar: onSidebar, accent: theme))
|
|
|
|
|
+ for s in CVPreviewDemoContent.skillsList.prefix(5) {
|
|
|
|
|
+ sidebar.addArrangedSubview(makeLabel("\(skillPrefix)\(s)", font: .systemFont(ofSize: 6, weight: .semibold), color: onSidebar.withAlphaComponent(0.92), alignment: .left, maxLines: 1))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let main = NSStackView()
|
|
|
|
|
+ main.orientation = .vertical
|
|
|
|
|
+ main.spacing = 4 + CGFloat(idVariant % 3)
|
|
|
|
|
+ main.alignment = .leading
|
|
|
|
|
+ main.addArrangedSubview(creativeMainHeader(theme: theme))
|
|
|
|
|
+ main.addArrangedSubview(sectionHeading("PROFILE"))
|
|
|
|
|
+ main.addArrangedSubview(makeLabel(CVPreviewDemoContent.summary, font: .systemFont(ofSize: 6.2), color: palette.previewInk, alignment: .left, maxLines: 3))
|
|
|
|
|
+ main.addArrangedSubview(sectionHeading("IMPACT"))
|
|
|
|
|
+ main.addArrangedSubview(makeLabel("\(CVPreviewDemoContent.company) — \(CVPreviewDemoContent.title)", font: .systemFont(ofSize: 6.6, weight: .heavy), color: palette.previewInk, alignment: .left, maxLines: 2))
|
|
|
|
|
+ let bMark = (idVariant % 2 == 0) ? "— " : "▸ "
|
|
|
|
|
+ main.addArrangedSubview(makeLabel("\(bMark)\(CVPreviewDemoContent.bullet1)", font: .systemFont(ofSize: 6), color: palette.previewMuted, alignment: .left, maxLines: 2))
|
|
|
|
|
+ main.addArrangedSubview(makeLabel("\(bMark)\(CVPreviewDemoContent.bullet2)", font: .systemFont(ofSize: 6), color: palette.previewMuted, alignment: .left, maxLines: 2))
|
|
|
|
|
+
|
|
|
|
|
+ let sidebarMult = 0.32 + CGFloat(idVariant % 3) * 0.02
|
|
|
|
|
+
|
|
|
|
|
+ switch template.layout {
|
|
|
|
|
+ case .singleColumn:
|
|
|
|
|
+ let banner = NSView()
|
|
|
|
|
+ banner.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ banner.wantsLayer = true
|
|
|
|
|
+ banner.layer?.backgroundColor = theme.cgColor
|
|
|
|
|
+ banner.layer?.cornerRadius = idVariant % 4 == 1 ? 6 : 3
|
|
|
|
|
+ let inner = makeLabel(" \(CVPreviewDemoContent.fullName) · \(CVPreviewDemoContent.title)", font: .systemFont(ofSize: 6.5, weight: .bold), color: .white, alignment: .left, maxLines: 1)
|
|
|
|
|
+ inner.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ banner.addSubview(inner)
|
|
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
|
|
+ inner.leadingAnchor.constraint(equalTo: banner.leadingAnchor, constant: 5),
|
|
|
|
|
+ inner.trailingAnchor.constraint(lessThanOrEqualTo: banner.trailingAnchor, constant: -5),
|
|
|
|
|
+ inner.topAnchor.constraint(equalTo: banner.topAnchor, constant: 4 + CGFloat(idVariant % 2)),
|
|
|
|
|
+ inner.bottomAnchor.constraint(equalTo: banner.bottomAnchor, constant: -4)
|
|
|
|
|
+ ])
|
|
|
|
|
+ let col = NSStackView(views: [banner, main])
|
|
|
|
|
+ col.orientation = .vertical
|
|
|
|
|
+ col.spacing = 6
|
|
|
|
|
+ col.alignment = .leading
|
|
|
|
|
+ return col
|
|
|
|
|
+ case .twoColumn(let side, _):
|
|
|
|
|
+ let row = NSStackView()
|
|
|
|
|
+ row.orientation = .horizontal
|
|
|
|
|
+ row.spacing = 5 + CGFloat(idVariant % 3)
|
|
|
|
|
+ row.alignment = .top
|
|
|
|
|
+ if side == .leading {
|
|
|
|
|
+ row.addArrangedSubview(sidebar)
|
|
|
|
|
+ row.addArrangedSubview(main)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ row.addArrangedSubview(main)
|
|
|
|
|
+ row.addArrangedSubview(sidebar)
|
|
|
|
|
+ }
|
|
|
|
|
+ sidebar.widthAnchor.constraint(equalTo: row.widthAnchor, multiplier: sidebarMult).isActive = true
|
|
|
|
|
+ return row
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func creativeDeepBackground(theme: NSColor) -> NSColor {
|
|
|
|
|
+ let navy = NSColor(srgbRed: 0.08, green: 0.1, blue: 0.18, alpha: 1)
|
|
|
|
|
+ let plum = NSColor(srgbRed: 0.14, green: 0.07, blue: 0.24, alpha: 1)
|
|
|
|
|
+ switch idVariant % 4 {
|
|
|
|
|
+ case 0: return theme.blended(withFraction: 0.52, of: navy) ?? theme
|
|
|
|
|
+ case 1: return theme.blended(withFraction: 0.7, of: NSColor.black) ?? theme
|
|
|
|
|
+ case 2: return palette.previewInk.blended(withFraction: 0.38, of: theme) ?? theme
|
|
|
|
|
+ default: return theme.blended(withFraction: 0.4, of: plum) ?? theme
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func creativeSidebarHeading(_ raw: String, onSidebar: NSColor, accent: NSColor) -> NSView {
|
|
|
|
|
+ let t = makeLabel(raw, font: .systemFont(ofSize: 5.5, weight: .heavy), color: onSidebar, alignment: .left, maxLines: 1)
|
|
|
|
|
+ let bar = NSView()
|
|
|
|
|
+ bar.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ bar.wantsLayer = true
|
|
|
|
|
+ bar.layer?.backgroundColor = accent.cgColor
|
|
|
|
|
+ bar.heightAnchor.constraint(equalToConstant: 2).isActive = true
|
|
|
|
|
+ let c = NSStackView(views: [t, bar])
|
|
|
|
|
+ c.orientation = .vertical
|
|
|
|
|
+ c.spacing = 2
|
|
|
|
|
+ c.alignment = .leading
|
|
|
|
|
+ bar.leadingAnchor.constraint(equalTo: t.leadingAnchor).isActive = true
|
|
|
|
|
+ bar.trailingAnchor.constraint(lessThanOrEqualTo: c.trailingAnchor).isActive = true
|
|
|
|
|
+ bar.widthAnchor.constraint(equalToConstant: 56).isActive = true
|
|
|
|
|
+ return c
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func creativeMainHeader(theme: NSColor) -> NSView {
|
|
|
|
|
+ let v = NSView()
|
|
|
|
|
+ v.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ v.wantsLayer = true
|
|
|
|
|
+ v.layer?.borderColor = theme.cgColor
|
|
|
|
|
+ v.layer?.borderWidth = 0
|
|
|
|
|
+ let stripe = NSView()
|
|
|
|
|
+ stripe.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ stripe.wantsLayer = true
|
|
|
|
|
+ stripe.layer?.backgroundColor = theme.cgColor
|
|
|
|
|
+ v.addSubview(stripe)
|
|
|
|
|
+ let row = NSStackView()
|
|
|
|
|
+ row.orientation = .horizontal
|
|
|
|
|
+ row.spacing = 5
|
|
|
|
|
+ row.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ let lab = makeLabel("PORTFOLIO SNAPSHOT", font: .systemFont(ofSize: 6.5, weight: .black), color: palette.previewInk, alignment: .left, maxLines: 1)
|
|
|
|
|
+ row.addArrangedSubview(stripe)
|
|
|
|
|
+ row.addArrangedSubview(lab)
|
|
|
|
|
+ v.addSubview(row)
|
|
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
|
|
+ stripe.widthAnchor.constraint(equalToConstant: 3),
|
|
|
|
|
+ stripe.heightAnchor.constraint(equalToConstant: 12),
|
|
|
|
|
+ row.leadingAnchor.constraint(equalTo: v.leadingAnchor),
|
|
|
|
|
+ row.topAnchor.constraint(equalTo: v.topAnchor),
|
|
|
|
|
+ row.bottomAnchor.constraint(equalTo: v.bottomAnchor)
|
|
|
|
|
+ ])
|
|
|
|
|
+ return v
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // MARK: - Shared pieces
|
|
|
|
|
+
|
|
|
|
|
+ private func sectionHeading(_ raw: String) -> NSTextField {
|
|
|
|
|
+ let s = formattedSectionLabel(raw)
|
|
|
|
|
+ let accent = accentDecorationColor()
|
|
|
|
|
+ return makeLabel(s, font: .systemFont(ofSize: 5.8, weight: .bold), color: accent, alignment: .left, maxLines: 1)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func formattedSectionLabel(_ raw: String) -> String {
|
|
|
|
|
+ switch template.sectionLabelStyle {
|
|
|
|
|
+ case .uppercase: return raw
|
|
|
|
|
+ case .slashed: return "// \(raw.capitalized)"
|
|
|
|
|
+ case .bracketed: return "[ \(raw) ]"
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func accentDecorationColor() -> NSColor {
|
|
|
|
|
+ switch template.accent {
|
|
|
|
|
+ case .redUnderline, .redBar: return palette.previewAccentRed
|
|
|
|
|
+ case .blueBar: return palette.previewAccentBlue
|
|
|
|
|
+ case .none: return template.themeColor.blended(withFraction: 0.5, of: palette.previewInk) ?? palette.previewInk
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func headlineAccent(theme: NSColor, width: CGFloat) -> NSView {
|
|
|
|
|
+ let v = NSView()
|
|
|
|
|
+ v.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ v.wantsLayer = true
|
|
|
|
|
+ switch template.accent {
|
|
|
|
|
+ case .none:
|
|
|
|
|
+ v.layer?.backgroundColor = theme.withAlphaComponent(0.35).cgColor
|
|
|
|
|
+ case .redUnderline:
|
|
|
|
|
+ v.layer?.backgroundColor = palette.previewAccentRed.cgColor
|
|
|
|
|
+ case .redBar:
|
|
|
|
|
+ v.layer?.backgroundColor = palette.previewAccentRed.cgColor
|
|
|
|
|
+ case .blueBar:
|
|
|
|
|
+ v.layer?.backgroundColor = palette.previewAccentBlue.cgColor
|
|
|
|
|
+ }
|
|
|
|
|
+ v.heightAnchor.constraint(equalToConstant: template.accent == .redUnderline ? 1.2 : 2.2).isActive = true
|
|
|
|
|
+ v.widthAnchor.constraint(equalToConstant: 72 * width).isActive = true
|
|
|
|
|
+ return v
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func hairline() -> NSView {
|
|
|
|
|
+ let v = NSView()
|
|
|
|
|
+ v.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ v.wantsLayer = true
|
|
|
|
|
+ v.layer?.backgroundColor = palette.previewMuted.withAlphaComponent(0.35).cgColor
|
|
|
|
|
+ v.heightAnchor.constraint(equalToConstant: 1).isActive = true
|
|
|
|
|
+ return v
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func hairlineSoft() -> NSView {
|
|
|
|
|
+ let v = NSView()
|
|
|
|
|
+ v.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ v.wantsLayer = true
|
|
|
|
|
+ v.layer?.backgroundColor = palette.previewMuted.withAlphaComponent(0.22).cgColor
|
|
|
|
|
+ v.heightAnchor.constraint(equalToConstant: 1).isActive = true
|
|
|
|
|
+ return v
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func initialsAvatar(diameter: CGFloat, ink: NSColor) -> NSView {
|
|
|
|
|
+ let avatar = NSView()
|
|
|
|
|
+ avatar.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ avatar.wantsLayer = true
|
|
|
|
|
+ avatar.layer?.backgroundColor = template.themeColor.withAlphaComponent(0.12).cgColor
|
|
|
|
|
+ avatar.layer?.borderColor = template.themeColor.withAlphaComponent(0.35).cgColor
|
|
|
|
|
+ avatar.layer?.borderWidth = 1
|
|
|
|
|
+ avatar.layer?.cornerRadius = diameter / 2
|
|
|
|
|
+ avatar.widthAnchor.constraint(equalToConstant: diameter).isActive = true
|
|
|
|
|
+ avatar.heightAnchor.constraint(equalToConstant: diameter).isActive = true
|
|
|
|
|
+ let initials = makeLabel("SJ", font: .systemFont(ofSize: diameter * 0.32, weight: .bold), color: ink, alignment: .center, maxLines: 1)
|
|
|
|
|
+ initials.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ avatar.addSubview(initials)
|
|
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
|
|
+ initials.centerXAnchor.constraint(equalTo: avatar.centerXAnchor),
|
|
|
|
|
+ initials.centerYAnchor.constraint(equalTo: avatar.centerYAnchor)
|
|
|
|
|
+ ])
|
|
|
|
|
+ return avatar
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func skillLineBullet(_ text: String) -> NSView {
|
|
|
|
|
+ makeLabel("· \(text)", font: .systemFont(ofSize: 6.1, weight: .regular), color: palette.previewInk, alignment: .left, maxLines: 1)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func bulletRow(_ text: String, size: CGFloat) -> NSView {
|
|
|
|
|
+ let row = NSStackView()
|
|
|
|
|
+ row.orientation = .horizontal
|
|
|
|
|
+ row.spacing = 3
|
|
|
|
|
+ row.alignment = .top
|
|
|
|
|
+ let mark = makeLabel("•", font: .systemFont(ofSize: size, weight: .bold), color: template.themeColor, alignment: .left, maxLines: 1)
|
|
|
|
|
+ mark.setContentHuggingPriority(.required, for: .horizontal)
|
|
|
|
|
+ let body = makeLabel(text, font: .systemFont(ofSize: size), color: palette.previewInk, alignment: .left, maxLines: 3)
|
|
|
|
|
+ row.addArrangedSubview(mark)
|
|
|
|
|
+ row.addArrangedSubview(body)
|
|
|
|
|
+ return row
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func makeLabel(
|
|
|
|
|
+ _ text: String,
|
|
|
|
|
+ font: NSFont,
|
|
|
|
|
+ color: NSColor,
|
|
|
|
|
+ alignment: NSTextAlignment,
|
|
|
|
|
+ maxLines: Int
|
|
|
|
|
+ ) -> NSTextField {
|
|
|
|
|
+ let tf: NSTextField
|
|
|
|
|
+ if maxLines == 1 {
|
|
|
|
|
+ tf = NSTextField(labelWithString: text)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ tf = NSTextField(wrappingLabelWithString: text)
|
|
|
|
|
+ tf.maximumNumberOfLines = maxLines
|
|
|
|
|
+ }
|
|
|
|
|
+ tf.font = font
|
|
|
|
|
+ tf.textColor = color
|
|
|
|
|
+ tf.alignment = alignment
|
|
|
|
|
+ tf.isEditable = false
|
|
|
|
|
+ tf.isSelectable = false
|
|
|
|
|
+ tf.drawsBackground = false
|
|
|
|
|
+ tf.isBordered = false
|
|
|
|
|
+ tf.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
|
|
|
+ return tf
|
|
|
|
|
+ }
|
|
|
|
|
+}
|