|
|
@@ -38,6 +38,12 @@ private struct DocumentStyle {
|
|
|
|
|
|
static func make(for template: CVTemplate) -> DocumentStyle {
|
|
|
let theme = template.themeColor
|
|
|
+ let colors = CVResumeAppearance.colors()
|
|
|
+ let sectionInk = CVResumeAppearance.sectionHeadingColor(for: template)
|
|
|
+ let cardBG = CVResumeAppearance.paperBackground(
|
|
|
+ variant: template.galleryLayoutVariant,
|
|
|
+ base: colors.cardBackground
|
|
|
+ )
|
|
|
switch template.family {
|
|
|
case .minimal:
|
|
|
return DocumentStyle(
|
|
|
@@ -54,14 +60,14 @@ private struct DocumentStyle {
|
|
|
bulletBodyFont: .systemFont(ofSize: 12, weight: .regular),
|
|
|
bulletMarkerFont: .systemFont(ofSize: 11, weight: .light),
|
|
|
bulletMarkerColor: theme.withAlphaComponent(0.55),
|
|
|
- ink: NSColor(srgbRed: 42 / 255, green: 48 / 255, blue: 56 / 255, alpha: 1),
|
|
|
- muted: NSColor(srgbRed: 110 / 255, green: 118 / 255, blue: 128 / 255, alpha: 1),
|
|
|
- rule: NSColor(srgbRed: 228 / 255, green: 230 / 255, blue: 234 / 255, alpha: 1),
|
|
|
- cardBackground: NSColor(srgbRed: 0.998, green: 0.998, blue: 0.998, alpha: 1),
|
|
|
+ ink: colors.ink,
|
|
|
+ muted: colors.muted,
|
|
|
+ rule: colors.rule,
|
|
|
+ cardBackground: cardBG,
|
|
|
columnVerticalSpacing: 15,
|
|
|
bodyBlockSpacing: 15,
|
|
|
roleUsesThemeColor: false,
|
|
|
- sectionInk: theme.withAlphaComponent(0.92)
|
|
|
+ sectionInk: sectionInk
|
|
|
)
|
|
|
|
|
|
case .professional:
|
|
|
@@ -78,15 +84,15 @@ private struct DocumentStyle {
|
|
|
eduMetaFont: .systemFont(ofSize: 11.5, weight: .medium),
|
|
|
bulletBodyFont: .systemFont(ofSize: 12, weight: .regular),
|
|
|
bulletMarkerFont: .systemFont(ofSize: 12, weight: .bold),
|
|
|
- bulletMarkerColor: NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 0.55),
|
|
|
- ink: NSColor(srgbRed: 28 / 255, green: 36 / 255, blue: 48 / 255, alpha: 1),
|
|
|
- muted: NSColor(srgbRed: 88 / 255, green: 98 / 255, blue: 118 / 255, alpha: 1),
|
|
|
- rule: NSColor(srgbRed: 210 / 255, green: 218 / 255, blue: 232 / 255, alpha: 1),
|
|
|
- cardBackground: NSColor.white,
|
|
|
+ bulletMarkerColor: theme,
|
|
|
+ ink: colors.ink,
|
|
|
+ muted: colors.muted,
|
|
|
+ rule: colors.rule,
|
|
|
+ cardBackground: cardBG,
|
|
|
columnVerticalSpacing: 13,
|
|
|
bodyBlockSpacing: 13,
|
|
|
roleUsesThemeColor: false,
|
|
|
- sectionInk: theme
|
|
|
+ sectionInk: sectionInk
|
|
|
)
|
|
|
|
|
|
case .modern:
|
|
|
@@ -104,14 +110,14 @@ private struct DocumentStyle {
|
|
|
bulletBodyFont: .systemFont(ofSize: 12.5, weight: .regular),
|
|
|
bulletMarkerFont: .systemFont(ofSize: 13, weight: .bold),
|
|
|
bulletMarkerColor: theme,
|
|
|
- ink: NSColor(srgbRed: 24 / 255, green: 34 / 255, blue: 52 / 255, alpha: 1),
|
|
|
- muted: NSColor(srgbRed: 96 / 255, green: 110 / 255, blue: 132 / 255, alpha: 1),
|
|
|
- rule: NSColor(srgbRed: 200 / 255, green: 214 / 255, blue: 236 / 255, alpha: 1),
|
|
|
- cardBackground: NSColor(srgbRed: 0.99, green: 0.995, blue: 1, alpha: 1),
|
|
|
+ ink: colors.ink,
|
|
|
+ muted: colors.muted,
|
|
|
+ rule: colors.rule,
|
|
|
+ cardBackground: cardBG,
|
|
|
columnVerticalSpacing: 17,
|
|
|
bodyBlockSpacing: 16,
|
|
|
roleUsesThemeColor: true,
|
|
|
- sectionInk: theme
|
|
|
+ sectionInk: sectionInk
|
|
|
)
|
|
|
|
|
|
case .executive:
|
|
|
@@ -125,6 +131,9 @@ private struct DocumentStyle {
|
|
|
?? NSFontManager.shared.convert(georgia12, toHaveTrait: .italicFontMask)
|
|
|
let eduMeta = NSFont(name: "Georgia-Italic", size: 11.5)
|
|
|
?? NSFontManager.shared.convert(georgia115, toHaveTrait: .italicFontMask)
|
|
|
+ let execCard = CVResumeAppearance.isDark
|
|
|
+ ? cardBG
|
|
|
+ : NSColor(srgbRed: 0.992, green: 0.99, blue: 0.985, alpha: 1)
|
|
|
return DocumentStyle(
|
|
|
nameFont: serifName,
|
|
|
roleFont: serifRole,
|
|
|
@@ -138,15 +147,15 @@ private struct DocumentStyle {
|
|
|
eduMetaFont: eduMeta,
|
|
|
bulletBodyFont: serifCompact,
|
|
|
bulletMarkerFont: .systemFont(ofSize: 11, weight: .bold),
|
|
|
- bulletMarkerColor: NSColor(srgbRed: 55 / 255, green: 55 / 255, blue: 62 / 255, alpha: 1),
|
|
|
- ink: NSColor(srgbRed: 22 / 255, green: 22 / 255, blue: 28 / 255, alpha: 1),
|
|
|
- muted: NSColor(srgbRed: 82 / 255, green: 82 / 255, blue: 90 / 255, alpha: 1),
|
|
|
- rule: NSColor(srgbRed: 72 / 255, green: 72 / 255, blue: 78 / 255, alpha: 0.35),
|
|
|
- cardBackground: NSColor(srgbRed: 0.992, green: 0.99, blue: 0.985, alpha: 1),
|
|
|
+ bulletMarkerColor: colors.ink.withAlphaComponent(0.75),
|
|
|
+ ink: colors.ink,
|
|
|
+ muted: colors.muted,
|
|
|
+ rule: colors.rule,
|
|
|
+ cardBackground: execCard,
|
|
|
columnVerticalSpacing: 18,
|
|
|
bodyBlockSpacing: 17,
|
|
|
roleUsesThemeColor: false,
|
|
|
- sectionInk: NSColor(srgbRed: 32 / 255, green: 32 / 255, blue: 38 / 255, alpha: 1)
|
|
|
+ sectionInk: sectionInk
|
|
|
)
|
|
|
|
|
|
case .creative:
|
|
|
@@ -163,15 +172,15 @@ private struct DocumentStyle {
|
|
|
eduMetaFont: .systemFont(ofSize: 12, weight: .medium),
|
|
|
bulletBodyFont: .systemFont(ofSize: 12.5, weight: .regular),
|
|
|
bulletMarkerFont: .systemFont(ofSize: 13, weight: .heavy),
|
|
|
- bulletMarkerColor: NSColor(srgbRed: 207 / 255, green: 67 / 255, blue: 50 / 255, alpha: 1),
|
|
|
- ink: NSColor(srgbRed: 32 / 255, green: 26 / 255, blue: 52 / 255, alpha: 1),
|
|
|
- muted: NSColor(srgbRed: 108 / 255, green: 96 / 255, blue: 130 / 255, alpha: 1),
|
|
|
+ bulletMarkerColor: CVResumeAppearance.accentColor(for: template),
|
|
|
+ ink: colors.ink,
|
|
|
+ muted: colors.muted,
|
|
|
rule: theme.withAlphaComponent(0.22),
|
|
|
- cardBackground: NSColor(srgbRed: 0.995, green: 0.993, blue: 1, alpha: 1),
|
|
|
+ cardBackground: cardBG,
|
|
|
columnVerticalSpacing: 18,
|
|
|
bodyBlockSpacing: 17,
|
|
|
roleUsesThemeColor: true,
|
|
|
- sectionInk: NSColor(srgbRed: 207 / 255, green: 67 / 255, blue: 50 / 255, alpha: 1)
|
|
|
+ sectionInk: sectionInk
|
|
|
)
|
|
|
}
|
|
|
}
|
|
|
@@ -187,9 +196,12 @@ final class CVProfileDocumentView: NSView {
|
|
|
|
|
|
private let profile: SavedProfile
|
|
|
private let template: CVTemplate
|
|
|
- private let style: DocumentStyle
|
|
|
+ private var style: DocumentStyle
|
|
|
/// Matches `CVTemplatePreviewView` so the same template id + layout recipe renders the same silhouette as the gallery card.
|
|
|
private let variant: Int
|
|
|
+ private var appearanceObserver: NSObjectProtocol?
|
|
|
+ private weak var cardView: NSView?
|
|
|
+
|
|
|
init(profile: SavedProfile, template: CVTemplate) {
|
|
|
self.profile = profile
|
|
|
self.template = template
|
|
|
@@ -200,9 +212,35 @@ final class CVProfileDocumentView: NSView {
|
|
|
wantsLayer = true
|
|
|
layer?.backgroundColor = NSColor.clear.cgColor
|
|
|
userInterfaceLayoutDirection = .leftToRight
|
|
|
- // Let the preview stack stretch us to the scroll view width; don’t shrink to label intrinsic widths.
|
|
|
setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
+ installCardContent()
|
|
|
+ appearanceObserver = NotificationCenter.default.addObserver(
|
|
|
+ forName: AppAppearanceManager.didChangeNotification,
|
|
|
+ object: nil,
|
|
|
+ queue: .main
|
|
|
+ ) { [weak self] _ in
|
|
|
+ self?.refreshForAppearanceChange()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ deinit {
|
|
|
+ if let appearanceObserver {
|
|
|
+ NotificationCenter.default.removeObserver(appearanceObserver)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ override func viewDidChangeEffectiveAppearance() {
|
|
|
+ super.viewDidChangeEffectiveAppearance()
|
|
|
+ refreshForAppearanceChange()
|
|
|
+ }
|
|
|
|
|
|
+ private func refreshForAppearanceChange() {
|
|
|
+ style = DocumentStyle.make(for: template)
|
|
|
+ installCardContent()
|
|
|
+ }
|
|
|
+
|
|
|
+ private func installCardContent() {
|
|
|
+ subviews.forEach { $0.removeFromSuperview() }
|
|
|
let card = NSView()
|
|
|
card.translatesAutoresizingMaskIntoConstraints = false
|
|
|
card.wantsLayer = true
|
|
|
@@ -211,6 +249,7 @@ final class CVProfileDocumentView: NSView {
|
|
|
card.layer?.borderWidth = 1
|
|
|
card.layer?.borderColor = style.rule.cgColor
|
|
|
card.layer?.masksToBounds = true
|
|
|
+ cardView = card
|
|
|
|
|
|
let root = buildRoot()
|
|
|
root.translatesAutoresizingMaskIntoConstraints = false
|
|
|
@@ -278,8 +317,12 @@ final class CVProfileDocumentView: NSView {
|
|
|
return buildModernFamilyDocument()
|
|
|
case .creative:
|
|
|
return buildCreativeFamilyDocument()
|
|
|
- case .professional, .minimal, .executive:
|
|
|
- return buildTraditionalFamilyDocument()
|
|
|
+ case .executive:
|
|
|
+ return buildExecutiveDocument()
|
|
|
+ case .minimal:
|
|
|
+ return buildMinimalDocument()
|
|
|
+ case .professional:
|
|
|
+ return buildProfessionalDocument()
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -550,7 +593,7 @@ final class CVProfileDocumentView: NSView {
|
|
|
let iv = NSImageView(image: img)
|
|
|
iv.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 12, weight: .semibold)
|
|
|
iv.contentTintColor = theme
|
|
|
- let t = label(title.uppercased(), font: style.sectionFont, color: style.sectionInk, maxLines: 1)
|
|
|
+ let t = label(title.uppercased(), font: style.sectionFont, color: style.ink, maxLines: 1)
|
|
|
let r = NSStackView(views: [iv, t])
|
|
|
r.orientation = .horizontal
|
|
|
r.spacing = 8
|
|
|
@@ -762,9 +805,300 @@ final class CVProfileDocumentView: NSView {
|
|
|
return stack
|
|
|
}
|
|
|
|
|
|
- // MARK: - Traditional families (professional / minimal / executive)
|
|
|
+ // MARK: - Executive (matches gallery serif layout)
|
|
|
+
|
|
|
+ private func buildExecutiveDocument() -> NSView {
|
|
|
+ let centeredHead = (variant % 2) == 0
|
|
|
+ let nameText = displayable(profile.personal.fullName, placeholder: "Your name")
|
|
|
+ let roleText = displayable(profile.personal.jobTitle, placeholder: "Professional headline")
|
|
|
+ let contactParts = [profile.personal.email, profile.personal.phone, profile.personal.address].filter {
|
|
|
+ !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
|
|
+ }
|
|
|
+ let contactText = contactParts.isEmpty ? "Add contact details in your profile" : contactParts.joined(separator: " · ")
|
|
|
+
|
|
|
+ let name = label(nameText, font: style.nameFont, color: style.ink, maxLines: 2)
|
|
|
+ let role = label(roleText, font: style.roleFont, color: style.muted, maxLines: 2)
|
|
|
+ let contact = label(contactText, font: style.contactFont, color: style.muted.withAlphaComponent(0.9), maxLines: 3)
|
|
|
+ if centeredHead {
|
|
|
+ name.alignment = .center
|
|
|
+ role.alignment = .center
|
|
|
+ contact.alignment = .center
|
|
|
+ }
|
|
|
+
|
|
|
+ let rule = executiveHeaderRule(wide: variant % 3 == 0)
|
|
|
+ let head = NSStackView(views: [name, role, contact, rule])
|
|
|
+ head.orientation = .vertical
|
|
|
+ head.spacing = 8
|
|
|
+ head.alignment = centeredHead ? .centerX : .leading
|
|
|
+
|
|
|
+ let body: NSView
|
|
|
+ switch template.layout {
|
|
|
+ case .singleColumn:
|
|
|
+ body = executiveMainColumn(compact: false, tightLeading: variant % 5 == 2)
|
|
|
+ case .twoColumn(let side, let tinted):
|
|
|
+ let main = executiveMainColumn(compact: true, tightLeading: variant % 5 == 2)
|
|
|
+ let sideCol = executiveSidebarColumn(tinted: tinted, showMetrics: variant % 4 == 1)
|
|
|
+ let row = NSStackView()
|
|
|
+ row.orientation = .horizontal
|
|
|
+ row.spacing = 20
|
|
|
+ row.alignment = .top
|
|
|
+ if side == .leading {
|
|
|
+ row.addArrangedSubview(sideCol)
|
|
|
+ row.addArrangedSubview(main)
|
|
|
+ } else {
|
|
|
+ row.addArrangedSubview(main)
|
|
|
+ row.addArrangedSubview(sideCol)
|
|
|
+ }
|
|
|
+ let mult: CGFloat = (variant % 5 == 3) ? 0.38 : 0.33
|
|
|
+ sideCol.widthAnchor.constraint(equalTo: row.widthAnchor, multiplier: mult).isActive = true
|
|
|
+ body = row
|
|
|
+ }
|
|
|
+
|
|
|
+ let wrap = NSStackView(views: [head, body])
|
|
|
+ wrap.orientation = .vertical
|
|
|
+ wrap.spacing = style.columnVerticalSpacing
|
|
|
+ wrap.alignment = .leading
|
|
|
+ return wrap
|
|
|
+ }
|
|
|
+
|
|
|
+ private func executiveHeaderRule(wide: Bool) -> NSView {
|
|
|
+ let theme = template.themeColor
|
|
|
+ let v = NSView()
|
|
|
+ v.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ v.wantsLayer = true
|
|
|
+ v.layer?.backgroundColor = theme.withAlphaComponent(0.45).cgColor
|
|
|
+ v.heightAnchor.constraint(equalToConstant: wide ? 2 : 1.5).isActive = true
|
|
|
+ v.widthAnchor.constraint(equalToConstant: wide ? 160 : 110).isActive = true
|
|
|
+ return v
|
|
|
+ }
|
|
|
+
|
|
|
+ private func executiveMainColumn(compact: Bool, tightLeading: Bool) -> NSView {
|
|
|
+ let stack = NSStackView()
|
|
|
+ stack.orientation = .vertical
|
|
|
+ stack.spacing = (compact ? style.bodyBlockSpacing : style.bodyBlockSpacing + 2) - (tightLeading ? 2 : 0)
|
|
|
+ stack.alignment = .leading
|
|
|
+
|
|
|
+ let summaryTitle = (variant % 6 == 3) ? "Summary" : "Professional Summary"
|
|
|
+ if let summary = nonEmpty(profile.careerSummary) {
|
|
|
+ stack.addArrangedSubview(sectionHeading(summaryTitle))
|
|
|
+ stack.addArrangedSubview(paragraph(summary, compact: compact))
|
|
|
+ }
|
|
|
+
|
|
|
+ let jobs = profile.workExperiences.filter { !$0.isEffectivelyEmpty }
|
|
|
+ if !jobs.isEmpty {
|
|
|
+ stack.addArrangedSubview(sectionHeading("Selected Experience"))
|
|
|
+ for job in jobs {
|
|
|
+ stack.addArrangedSubview(executiveExperienceBlock(job: job, compact: compact))
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ let schools = profile.educations.filter { !$0.isEffectivelyEmpty }
|
|
|
+ if !schools.isEmpty {
|
|
|
+ stack.addArrangedSubview(sectionHeading("Education"))
|
|
|
+ for edu in schools {
|
|
|
+ stack.addArrangedSubview(educationBlock(edu: edu, compact: compact))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ appendCertificatesInterestsReferrals(to: stack, compact: compact)
|
|
|
+ return stack
|
|
|
+ }
|
|
|
+
|
|
|
+ private func executiveExperienceBlock(job: WorkExperiencePayload, compact: Bool) -> NSView {
|
|
|
+ let v = NSStackView()
|
|
|
+ v.orientation = .vertical
|
|
|
+ v.spacing = 4
|
|
|
+ v.alignment = .leading
|
|
|
+ let titleLine = [job.jobTitle, job.company].filter { !$0.isEmpty }.joined(separator: ", ")
|
|
|
+ if !titleLine.isEmpty {
|
|
|
+ v.addArrangedSubview(label(titleLine, font: style.expTitleFont, color: style.ink, maxLines: 0))
|
|
|
+ }
|
|
|
+ let duration = job.duration.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
+ if !duration.isEmpty {
|
|
|
+ v.addArrangedSubview(label(duration, font: style.expMetaFont, color: template.themeColor, maxLines: 0))
|
|
|
+ }
|
|
|
+ for bullet in Self.bulletChunks(from: job.description) {
|
|
|
+ v.addArrangedSubview(label(bullet, font: style.bodyFont, color: style.muted, maxLines: 0))
|
|
|
+ }
|
|
|
+ return v
|
|
|
+ }
|
|
|
+
|
|
|
+ private func executiveSidebarColumn(tinted: Bool, showMetrics: Bool) -> NSView {
|
|
|
+ let stack = NSStackView()
|
|
|
+ stack.orientation = .vertical
|
|
|
+ stack.spacing = 10
|
|
|
+ stack.alignment = .leading
|
|
|
+ if tinted {
|
|
|
+ stack.wantsLayer = true
|
|
|
+ let fill = (variant % 3 == 0)
|
|
|
+ ? CVResumeAppearance.colors().sidebarTint
|
|
|
+ : template.themeColor.withAlphaComponent(0.07)
|
|
|
+ stack.layer?.backgroundColor = fill.cgColor
|
|
|
+ stack.layer?.cornerRadius = variant % 4 == 2 ? 8 : 6
|
|
|
+ stack.edgeInsets = NSEdgeInsets(top: 14, left: 14, bottom: 14, right: 14)
|
|
|
+ }
|
|
|
+
|
|
|
+ let skills = skillTokensFromProfile(max: 12)
|
|
|
+ if !skills.isEmpty {
|
|
|
+ stack.addArrangedSubview(sectionHeading("Core Competencies"))
|
|
|
+ for s in skills {
|
|
|
+ stack.addArrangedSubview(label("· \(s)", font: style.bodyFont, color: style.ink, maxLines: 0))
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if let cert = nonEmpty(profile.certificates) {
|
|
|
+ stack.addArrangedSubview(sectionHeading("Tools"))
|
|
|
+ stack.addArrangedSubview(paragraph(cert, compact: true))
|
|
|
+ }
|
|
|
+
|
|
|
+ if showMetrics, let hi = highlightsBodyText() {
|
|
|
+ stack.addArrangedSubview(sectionHeading("Impact"))
|
|
|
+ stack.addArrangedSubview(paragraph(hi, compact: true))
|
|
|
+ }
|
|
|
+
|
|
|
+ if stack.arrangedSubviews.isEmpty {
|
|
|
+ stack.addArrangedSubview(sectionHeading("Contact"))
|
|
|
+ for line in contactLines() {
|
|
|
+ stack.addArrangedSubview(label(line, font: style.contactFont, color: style.ink, maxLines: 0))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return stack
|
|
|
+ }
|
|
|
+
|
|
|
+ // MARK: - Minimal (matches gallery light typography)
|
|
|
+
|
|
|
+ private func buildMinimalDocument() -> NSView {
|
|
|
+ let nameText = displayable(profile.personal.fullName, placeholder: "Your name")
|
|
|
+ let roleText = displayable(profile.personal.jobTitle, placeholder: "Professional headline")
|
|
|
+ let contactParts = [profile.personal.email, profile.personal.phone].filter {
|
|
|
+ !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
|
|
+ }
|
|
|
+ let contactText = contactParts.isEmpty ? "Add contact in your profile" : contactParts.joined(separator: " ")
|
|
|
+
|
|
|
+ let nameWeight: NSFont.Weight = (variant % 3 == 0) ? .ultraLight : .light
|
|
|
+ let nameSize: CGFloat = template.headline == .centered ? 22 + CGFloat(variant % 2) : 20 + CGFloat(variant % 3)
|
|
|
+ let name = label(nameText, font: .systemFont(ofSize: nameSize, weight: nameWeight), color: style.ink, maxLines: 2)
|
|
|
+ let role = label(roleText.uppercased(), font: .systemFont(ofSize: 12.5, weight: .medium), color: style.muted, maxLines: 2)
|
|
|
+ let contact = label(contactText, font: style.contactFont, color: style.muted.withAlphaComponent(0.75), maxLines: 2)
|
|
|
+
|
|
|
+ let head = NSStackView()
|
|
|
+ head.orientation = .vertical
|
|
|
+ head.spacing = template.headline == .avatarStacked ? 10 : 6 + CGFloat(variant % 4)
|
|
|
+ head.alignment = template.headline == .centered ? .centerX : .leading
|
|
|
+ if template.headline == .avatarStacked {
|
|
|
+ head.addArrangedSubview(initialsBadge(for: nameText))
|
|
|
+ }
|
|
|
+ if template.headline == .centered {
|
|
|
+ name.alignment = .center
|
|
|
+ role.alignment = .center
|
|
|
+ contact.alignment = .center
|
|
|
+ }
|
|
|
+ head.addArrangedSubview(name)
|
|
|
+ head.addArrangedSubview(role)
|
|
|
+ head.addArrangedSubview(contact)
|
|
|
+ head.addArrangedSubview(hairline())
|
|
|
+ if variant % 5 == 1 {
|
|
|
+ head.addArrangedSubview(hairline())
|
|
|
+ }
|
|
|
+
|
|
|
+ let swapEdu = (variant % 4) == 2
|
|
|
+ let body: NSView
|
|
|
+ switch template.layout {
|
|
|
+ case .singleColumn:
|
|
|
+ body = minimalMainColumn(spacing: style.bodyBlockSpacing, educationBeforeExperience: swapEdu)
|
|
|
+ case .twoColumn(let side, _):
|
|
|
+ let main = minimalMainColumn(spacing: style.bodyBlockSpacing - 2, educationBeforeExperience: swapEdu)
|
|
|
+ let aside = minimalSkillsAside(numbered: variant % 3 == 1)
|
|
|
+ let row = NSStackView()
|
|
|
+ row.orientation = .horizontal
|
|
|
+ row.spacing = 18 + CGFloat(variant % 3)
|
|
|
+ row.alignment = .top
|
|
|
+ if side == .leading {
|
|
|
+ row.addArrangedSubview(aside)
|
|
|
+ row.addArrangedSubview(main)
|
|
|
+ } else {
|
|
|
+ row.addArrangedSubview(main)
|
|
|
+ row.addArrangedSubview(aside)
|
|
|
+ }
|
|
|
+ let mult: CGFloat = (variant % 5 == 0) ? 0.34 : 0.30
|
|
|
+ aside.widthAnchor.constraint(equalTo: row.widthAnchor, multiplier: mult).isActive = true
|
|
|
+ body = row
|
|
|
+ }
|
|
|
+
|
|
|
+ let wrap = NSStackView(views: [head, body])
|
|
|
+ wrap.orientation = .vertical
|
|
|
+ wrap.spacing = style.columnVerticalSpacing
|
|
|
+ wrap.alignment = .leading
|
|
|
+ return wrap
|
|
|
+ }
|
|
|
+
|
|
|
+ private func minimalMainColumn(spacing: CGFloat, educationBeforeExperience: Bool) -> NSView {
|
|
|
+ let stack = NSStackView()
|
|
|
+ stack.orientation = .vertical
|
|
|
+ stack.spacing = spacing
|
|
|
+ stack.alignment = .leading
|
|
|
+
|
|
|
+ let appendSummary: () -> Void = { [self] in
|
|
|
+ if let summary = nonEmpty(profile.careerSummary) {
|
|
|
+ stack.addArrangedSubview(sectionHeading("Profile"))
|
|
|
+ stack.addArrangedSubview(paragraph(summary, compact: false))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ let appendExperience: () -> Void = { [self] in
|
|
|
+ let jobs = profile.workExperiences.filter { !$0.isEffectivelyEmpty }
|
|
|
+ if !jobs.isEmpty {
|
|
|
+ stack.addArrangedSubview(sectionHeading("Experience"))
|
|
|
+ for job in jobs {
|
|
|
+ stack.addArrangedSubview(experienceBlock(job: job, compact: false))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ let appendEducation: () -> Void = { [self] in
|
|
|
+ let schools = profile.educations.filter { !$0.isEffectivelyEmpty }
|
|
|
+ if !schools.isEmpty {
|
|
|
+ stack.addArrangedSubview(sectionHeading("Education"))
|
|
|
+ for edu in schools {
|
|
|
+ stack.addArrangedSubview(educationBlock(edu: edu, compact: false))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if educationBeforeExperience {
|
|
|
+ appendEducation()
|
|
|
+ appendSummary()
|
|
|
+ appendExperience()
|
|
|
+ } else {
|
|
|
+ appendSummary()
|
|
|
+ appendExperience()
|
|
|
+ appendEducation()
|
|
|
+ }
|
|
|
+ appendCertificatesInterestsReferrals(to: stack, compact: false)
|
|
|
+ return stack
|
|
|
+ }
|
|
|
+
|
|
|
+ private func minimalSkillsAside(numbered: Bool) -> NSView {
|
|
|
+ let stack = NSStackView()
|
|
|
+ stack.orientation = .vertical
|
|
|
+ stack.spacing = 8
|
|
|
+ stack.alignment = .leading
|
|
|
+ let skills = skillTokensFromProfile(max: 12)
|
|
|
+ if skills.isEmpty {
|
|
|
+ stack.addArrangedSubview(sectionHeading("Contact"))
|
|
|
+ for line in contactLines() {
|
|
|
+ stack.addArrangedSubview(label(line, font: style.contactFont, color: style.muted, maxLines: 0))
|
|
|
+ }
|
|
|
+ return stack
|
|
|
+ }
|
|
|
+ stack.addArrangedSubview(sectionHeading("Skills"))
|
|
|
+ for (i, s) in skills.enumerated() {
|
|
|
+ let prefix = numbered ? "\(i + 1). " : "· "
|
|
|
+ stack.addArrangedSubview(label("\(prefix)\(s)", font: style.bodyCompactFont, color: style.muted, maxLines: 0))
|
|
|
+ }
|
|
|
+ return stack
|
|
|
+ }
|
|
|
+
|
|
|
+ // MARK: - Professional
|
|
|
|
|
|
- private func buildTraditionalFamilyDocument() -> NSView {
|
|
|
+ private func buildProfessionalDocument() -> NSView {
|
|
|
switch template.layout {
|
|
|
case .singleColumn:
|
|
|
return singleColumnLayout()
|
|
|
@@ -840,7 +1174,7 @@ final class CVProfileDocumentView: NSView {
|
|
|
let row = NSStackView()
|
|
|
row.orientation = .horizontal
|
|
|
row.alignment = .top
|
|
|
- row.spacing = template.family == .minimal ? 18 : 22
|
|
|
+ row.spacing = 22
|
|
|
|
|
|
let sidebarCol = sidebarColumn(tinted: tinted)
|
|
|
let mainCol = bodyColumn(compact: true, experienceFirst: professionalExperienceFirst)
|
|
|
@@ -852,12 +1186,7 @@ final class CVProfileDocumentView: NSView {
|
|
|
row.addArrangedSubview(mainCol)
|
|
|
row.addArrangedSubview(sidebarCol)
|
|
|
}
|
|
|
- let sidebarMult: CGFloat
|
|
|
- if template.family == .professional {
|
|
|
- sidebarMult = (variant % 5 == 2) ? 0.38 : 0.32
|
|
|
- } else {
|
|
|
- sidebarMult = template.family == .executive ? 0.34 : 0.32
|
|
|
- }
|
|
|
+ let sidebarMult: CGFloat = (variant % 5 == 2) ? 0.38 : 0.32
|
|
|
sidebarCol.widthAnchor.constraint(equalTo: row.widthAnchor, multiplier: sidebarMult).isActive = true
|
|
|
|
|
|
v.addArrangedSubview(row)
|
|
|
@@ -965,12 +1294,12 @@ final class CVProfileDocumentView: NSView {
|
|
|
bar.heightAnchor.constraint(equalToConstant: 1).isActive = true
|
|
|
return bar
|
|
|
case .redUnderline:
|
|
|
- bar.layer?.backgroundColor = NSColor(srgbRed: 207 / 255, green: 67 / 255, blue: 50 / 255, alpha: 1).cgColor
|
|
|
+ bar.layer?.backgroundColor = CVResumeAppearance.accentColor(for: template).cgColor
|
|
|
bar.heightAnchor.constraint(equalToConstant: 2).isActive = true
|
|
|
bar.widthAnchor.constraint(equalToConstant: template.family == .minimal ? 140 : 168).isActive = true
|
|
|
return bar
|
|
|
case .redBar:
|
|
|
- bar.layer?.backgroundColor = NSColor(srgbRed: 207 / 255, green: 67 / 255, blue: 50 / 255, alpha: 1).cgColor
|
|
|
+ bar.layer?.backgroundColor = CVResumeAppearance.accentColor(for: template).cgColor
|
|
|
bar.heightAnchor.constraint(equalToConstant: 4).isActive = true
|
|
|
bar.widthAnchor.constraint(equalToConstant: 56).isActive = true
|
|
|
return bar
|
|
|
@@ -1004,8 +1333,11 @@ final class CVProfileDocumentView: NSView {
|
|
|
box.alignment = .leading
|
|
|
if tinted {
|
|
|
box.wantsLayer = true
|
|
|
- box.layer?.backgroundColor = template.themeColor.withAlphaComponent(template.family == .creative ? 0.12 : 0.08).cgColor
|
|
|
- box.layer?.cornerRadius = 8
|
|
|
+ let tint = (variant % 4 == 1)
|
|
|
+ ? template.themeColor.withAlphaComponent(0.08)
|
|
|
+ : CVResumeAppearance.colors().sidebarTint
|
|
|
+ box.layer?.backgroundColor = tint.cgColor
|
|
|
+ box.layer?.cornerRadius = variant % 3 == 0 ? 8 : 6
|
|
|
}
|
|
|
box.edgeInsets = NSEdgeInsets(top: tinted ? 14 : 0, left: tinted ? 14 : 0, bottom: tinted ? 14 : 0, right: tinted ? 14 : 0)
|
|
|
|
|
|
@@ -1014,8 +1346,24 @@ final class CVProfileDocumentView: NSView {
|
|
|
box.addArrangedSubview(label(line, font: style.contactFont, color: style.ink, maxLines: 0))
|
|
|
}
|
|
|
|
|
|
- if let skillsBlock = ancillaryBlock(title: "Languages & more", body: combinedAncillaryText()) {
|
|
|
- box.addArrangedSubview(skillsBlock)
|
|
|
+ let skills = skillTokensFromProfile(max: 8)
|
|
|
+ if !skills.isEmpty {
|
|
|
+ box.addArrangedSubview(sectionHeading("Skills"))
|
|
|
+ if variant % 5 == 2 {
|
|
|
+ box.addArrangedSubview(skillTagRow(theme: template.themeColor, maxTags: 5))
|
|
|
+ } else {
|
|
|
+ for token in skills {
|
|
|
+ box.addArrangedSubview(label("· \(token)", font: style.contactFont, color: style.ink, maxLines: 0))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if variant % 7 == 3, let tools = nonEmpty(profile.certificates) {
|
|
|
+ box.addArrangedSubview(sectionHeading("Tools"))
|
|
|
+ box.addArrangedSubview(paragraph(tools, compact: true))
|
|
|
+ } else if let ancillary = combinedAncillaryText(), !ancillary.isEmpty {
|
|
|
+ box.addArrangedSubview(sectionHeading("Languages & more"))
|
|
|
+ box.addArrangedSubview(paragraph(ancillary, compact: true))
|
|
|
}
|
|
|
|
|
|
return box
|