// // MyProfilePageView.swift // App for Indeed // // Profile editor: card layout, adaptive two-column rows, and vertical scrolling // when the window is short. Follows the active dashboard light / dark appearance. // import Cocoa private enum ProfilePagePalette { static var brandBlue: NSColor { AppDashboardTheme.brandBlue } static var brandBlueHover: NSColor { AppDashboardTheme.brandBlueHover } static var pageBackground: NSColor { AppDashboardTheme.pageBackground } static var cardBackground: NSColor { AppDashboardTheme.cardBackground } static var fieldFill: NSColor { AppDashboardTheme.profileFieldFill } static var primaryText: NSColor { AppDashboardTheme.primaryText } static var secondaryText: NSColor { AppDashboardTheme.secondaryText } static var border: NSColor { AppDashboardTheme.border } static var destructive: NSColor { AppDashboardTheme.profileDestructive } static var ctaText: NSColor { AppDashboardTheme.proCTAText } } private enum ProfileThemeAppearance { static func refreshFormSubtree(_ root: NSView) { for view in root.profileSubviewsRecursive() { if let field = view as? NSTextField { if field.isEditable { field.textColor = ProfilePagePalette.primaryText if let placeholder = field.placeholderAttributedString?.string, !placeholder.isEmpty { field.placeholderAttributedString = NSAttributedString( string: placeholder, attributes: [ .foregroundColor: ProfilePagePalette.secondaryText, .font: field.font ?? NSFont.systemFont(ofSize: 14, weight: .regular), .paragraphStyle: ProfileLayoutEnforcement.leftAlignedParagraphStyle() ] ) } } else if let font = field.font { if font.pointSize >= 15 { field.textColor = ProfilePagePalette.primaryText } else { field.textColor = ProfilePagePalette.secondaryText } } } guard view.wantsLayer, let layer = view.layer else { continue } if layer.cornerRadius == 10, layer.borderWidth == 1 { layer.backgroundColor = ProfilePagePalette.fieldFill.cgColor layer.borderColor = ProfilePagePalette.border.cgColor } else if layer.cornerRadius == 14, layer.borderWidth == 1 { layer.backgroundColor = ProfilePagePalette.cardBackground.cgColor layer.borderColor = ProfilePagePalette.border.cgColor } } } } private extension NSView { func profileSubviewsRecursive() -> [NSView] { var result: [NSView] = [] var stack: [NSView] = [self] while let view = stack.popLast() { result.append(view) stack.append(contentsOf: view.subviews) } return result } } /// Keeps profile text left-aligned and LTR so fields do not collapse to a narrow trailing strip under RTL / natural alignment. private enum ProfileLayoutEnforcement { static func applyForcedLTRSubtree(from root: NSView) { var stack: [NSView] = [root] while let view = stack.popLast() { applyForcedLTR(to: view) stack.append(contentsOf: view.subviews) } } static func applyForcedLTR(to view: NSView) { view.userInterfaceLayoutDirection = .leftToRight } static func applyLeftAlignedTextField(_ field: NSTextField) { applyForcedLTR(to: field) field.baseWritingDirection = .leftToRight field.alignment = .left if let cell = field.cell as? NSTextFieldCell { cell.alignment = .left cell.baseWritingDirection = .leftToRight } } static func leftAlignedParagraphStyle() -> NSParagraphStyle { let p = NSMutableParagraphStyle() p.alignment = .left p.baseWritingDirection = .leftToRight return p } } private extension NSStackView { /// For vertical stacks using `.leading` alignment (geometric left under mixed RTL), pin each arranged subview’s width to the stack so labels/fields stay full-width. func pinAllArrangedSubviewWidthsEqualToStackWidth() { for subview in arrangedSubviews { subview.widthAnchor.constraint(equalTo: widthAnchor).isActive = true } } } /// Tags profile form controls with English localization keys so labels and placeholders refresh when the user changes language. private enum ProfileFormLocalization { static let labelPrefix = "profileForm.label." static let sectionPrefix = "profileForm.section." static let placeholderPrefix = "profileForm.placeholder." static let buttonPrefix = "profileForm.button." static func tagLabel(_ label: NSTextField, key: String) { label.identifier = NSUserInterfaceItemIdentifier(labelPrefix + key) } static func tagSection(_ label: NSTextField, key: String) { label.identifier = NSUserInterfaceItemIdentifier(sectionPrefix + key) } static func tagPlaceholder(_ field: NSTextField, key: String) { field.identifier = NSUserInterfaceItemIdentifier(placeholderPrefix + key) } static func tagButton(_ button: NSButton, key: String) { button.identifier = NSUserInterfaceItemIdentifier(buttonPrefix + key) } static func placeholderAttributes() -> [NSAttributedString.Key: Any] { [ .foregroundColor: ProfilePagePalette.secondaryText, .font: NSFont.systemFont(ofSize: 14, weight: .regular), .paragraphStyle: ProfileLayoutEnforcement.leftAlignedParagraphStyle() ] } static func applyPlaceholder(_ text: String, to field: NSTextField) { field.placeholderAttributedString = NSAttributedString(string: text, attributes: placeholderAttributes()) } static func refresh(in root: NSView) { for view in root.profileSubviewsRecursive() { guard let rawID = view.identifier?.rawValue else { continue } if let label = view as? NSTextField, !label.isEditable { if rawID.hasPrefix(labelPrefix) { label.stringValue = L(String(rawID.dropFirst(labelPrefix.count))) } else if rawID.hasPrefix(sectionPrefix) { label.stringValue = L(String(rawID.dropFirst(sectionPrefix.count))) } } else if let field = view as? NSTextField, field.isEditable, rawID.hasPrefix(placeholderPrefix) { applyPlaceholder(L(String(rawID.dropFirst(placeholderPrefix.count))), to: field) } else if let button = view as? NSButton, rawID.hasPrefix(buttonPrefix) { let key = String(rawID.dropFirst(buttonPrefix.count)) if button.image != nil { button.image = NSImage(systemSymbolName: "trash", accessibilityDescription: L(key)) } else { button.title = L(key) } } } } } /// Two fields side‑by‑side with a true 50/50 split, or stacked full‑width when compact. Avoids `NSStackView` collapsing paired columns to a narrow strip on the trailing edge. private final class ProfileDualFieldRow: NSView { private let leftView: NSView private let rightView: NSView private let spacing: CGFloat private var horizontalConstraints: [NSLayoutConstraint] = [] private var verticalConstraints: [NSLayoutConstraint] = [] private var isCompact = false init(left: NSView, right: NSView, spacing: CGFloat = 12) { self.leftView = left self.rightView = right self.spacing = spacing super.init(frame: .zero) translatesAutoresizingMaskIntoConstraints = false ProfileLayoutEnforcement.applyForcedLTR(to: self) addSubview(leftView) addSubview(rightView) leftView.translatesAutoresizingMaskIntoConstraints = false rightView.translatesAutoresizingMaskIntoConstraints = false leftView.setContentHuggingPriority(.defaultLow, for: .horizontal) rightView.setContentHuggingPriority(.defaultLow, for: .horizontal) leftView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) rightView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) horizontalConstraints = [ leftView.leftAnchor.constraint(equalTo: leftAnchor), leftView.topAnchor.constraint(equalTo: topAnchor), leftView.bottomAnchor.constraint(equalTo: bottomAnchor), rightView.rightAnchor.constraint(equalTo: rightAnchor), rightView.topAnchor.constraint(equalTo: topAnchor), rightView.bottomAnchor.constraint(equalTo: bottomAnchor), leftView.rightAnchor.constraint(equalTo: rightView.leftAnchor, constant: -spacing), leftView.widthAnchor.constraint(equalTo: rightView.widthAnchor) ] verticalConstraints = [ leftView.leftAnchor.constraint(equalTo: leftAnchor), leftView.rightAnchor.constraint(equalTo: rightAnchor), leftView.topAnchor.constraint(equalTo: topAnchor), rightView.leftAnchor.constraint(equalTo: leftAnchor), rightView.rightAnchor.constraint(equalTo: rightAnchor), rightView.topAnchor.constraint(equalTo: leftView.bottomAnchor, constant: spacing), rightView.bottomAnchor.constraint(equalTo: bottomAnchor) ] NSLayoutConstraint.activate(horizontalConstraints) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func setCompact(_ compact: Bool) { guard compact != isCompact else { return } isCompact = compact if compact { NSLayoutConstraint.deactivate(horizontalConstraints) NSLayoutConstraint.activate(verticalConstraints) } else { NSLayoutConstraint.deactivate(verticalConstraints) NSLayoutConstraint.activate(horizontalConstraints) } } } final class MyProfilePageView: NSView { /// Below this form content width, two-column rows stack vertically. private static let compactFormWidth: CGFloat = 640 private static let horizontalPageInset: CGFloat = 24 /// Inset of form content from the card border (left/right); explicit constraints so fields stay inside the card chrome. private static let cardContentHorizontalInset: CGFloat = 28 private let scrollView = NSScrollView() private let documentView = NSView() private let cardView = NSView() private let formStack = NSStackView() private let profileNameField = NSTextField() private let fullNameField = NSTextField() private let emailField = NSTextField() private let phoneField = NSTextField() private let jobTitleField = NSTextField() private let addressField = NSTextField() private let careerField = NSTextField() private let certificatesField = NSTextField() private let interestsField = NSTextField() private let languagesField = NSTextField() private let referralField = NSTextField() private let saveButton = ProfilePrimaryButton(title: L("Save Profile →"), target: nil, action: nil) private var nameEmailRow: ProfileDualFieldRow! private var phoneJobRow: ProfileDualFieldRow! private let topChrome = NSView() private let backButton = NSButton(title: L("← All profiles"), target: nil, action: nil) private let contextLabel = NSTextField(labelWithString: "") private var editingProfileID: UUID? /// Called from the back control and after a successful save (returns to the profiles list). var onDismiss: (() -> Void)? private let workExperienceRowsStack = NSStackView() private var workExperienceEntries: [WorkExperienceEntryView] = [] private let educationRowsStack = NSStackView() private var educationEntries: [EducationEntryView] = [] private var lastCompactLayout: Bool? private var referralHelperLabel: NSTextField? private var appearanceObserver: NSObjectProtocol? private var languageObserver: NSObjectProtocol? /// Force left-to-right geometry so profile fields span the full width even when the window uses RTL layout. override var userInterfaceLayoutDirection: NSUserInterfaceLayoutDirection { get { .leftToRight } set { super.userInterfaceLayoutDirection = .leftToRight } } override init(frame frameRect: NSRect) { super.init(frame: frameRect) setup() appearanceObserver = NotificationCenter.default.addObserver( forName: AppAppearanceManager.didChangeNotification, object: nil, queue: .main ) { [weak self] _ in self?.applyCurrentAppearance() } languageObserver = NotificationCenter.default.addObserver( forName: AppLanguageManager.didChangeNotification, object: nil, queue: .main ) { [weak self] _ in self?.applyLocalizedStrings() } applyCurrentAppearance() applyLocalizedStrings() } deinit { if let appearanceObserver { NotificationCenter.default.removeObserver(appearanceObserver) } if let languageObserver { NotificationCenter.default.removeObserver(languageObserver) } } required init?(coder: NSCoder) { super.init(coder: coder) setup() } override func viewDidChangeEffectiveAppearance() { super.viewDidChangeEffectiveAppearance() applyCurrentAppearance() } func applyCurrentAppearance() { layer?.backgroundColor = ProfilePagePalette.pageBackground.cgColor cardView.layer?.backgroundColor = ProfilePagePalette.cardBackground.cgColor cardView.layer?.borderColor = ProfilePagePalette.border.cgColor backButton.contentTintColor = ProfilePagePalette.brandBlue contextLabel.textColor = ProfilePagePalette.primaryText ProfileThemeAppearance.refreshFormSubtree(formStack) for entry in workExperienceEntries { entry.applyCurrentAppearance() } for entry in educationEntries { entry.applyCurrentAppearance() } saveButton.applyCurrentAppearance() } func applyLocalizedStrings() { backButton.title = L("← All profiles") saveButton.title = L("Save Profile →") contextLabel.stringValue = editingProfileID == nil ? L("New profile") : L("Edit profile") ProfileFormLocalization.refresh(in: formStack) referralHelperLabel?.stringValue = L("If someone referred you for this job, enter their name or company here") renumberWorkExperienceEntries() renumberEducationEntries() ProfileThemeAppearance.refreshFormSubtree(formStack) } override func viewDidMoveToWindow() { super.viewDidMoveToWindow() guard window != nil else { return } ProfileLayoutEnforcement.applyForcedLTRSubtree(from: self) needsLayout = true } override func layout() { super.layout() if let layer = cardView.layer, layer.shadowOpacity > 0 { let r = layer.cornerRadius layer.shadowPath = CGPath(roundedRect: cardView.bounds, cornerWidth: r, cornerHeight: r, transform: nil) } updateMultilinePreferredLayoutWidths() applyResponsiveRowsIfNeeded() } /// Wrapping `NSTextField`s report a tiny intrinsic width until `preferredMaxLayoutWidth` tracks the chrome width, which otherwise collapses the stack to a narrow trailing column. private func updateMultilinePreferredLayoutWidths() { let horizontalInset: CGFloat = 24 applyPreferredWrapWidth(to: profileNameField, horizontalInset: horizontalInset) applyPreferredWrapWidth(to: fullNameField, horizontalInset: horizontalInset) applyPreferredWrapWidth(to: emailField, horizontalInset: horizontalInset) applyPreferredWrapWidth(to: phoneField, horizontalInset: horizontalInset) applyPreferredWrapWidth(to: jobTitleField, horizontalInset: horizontalInset) applyPreferredWrapWidth(to: addressField, horizontalInset: horizontalInset) applyPreferredWrapWidth(to: referralField, horizontalInset: horizontalInset) applyPreferredWrapWidth(to: careerField, horizontalInset: horizontalInset) applyPreferredWrapWidth(to: certificatesField, horizontalInset: horizontalInset) applyPreferredWrapWidth(to: interestsField, horizontalInset: horizontalInset) applyPreferredWrapWidth(to: languagesField, horizontalInset: horizontalInset) if let helper = referralHelperLabel, let stack = helper.superview, stack.bounds.width > 2 { let w = max(1, stack.bounds.width - 8) if abs(helper.preferredMaxLayoutWidth - w) > 0.5 { helper.preferredMaxLayoutWidth = w } } } private func applyPreferredWrapWidth(to field: NSTextField, horizontalInset: CGFloat) { guard let wrap = field.superview, wrap.bounds.width > 2 else { return } let w = max(1, wrap.bounds.width - horizontalInset) if abs(field.preferredMaxLayoutWidth - w) > 0.5 { field.preferredMaxLayoutWidth = w } } private func setup() { wantsLayer = true layer?.backgroundColor = ProfilePagePalette.pageBackground.cgColor userInterfaceLayoutDirection = .leftToRight ProfileLayoutEnforcement.applyForcedLTR(to: self) scrollView.translatesAutoresizingMaskIntoConstraints = false scrollView.userInterfaceLayoutDirection = .leftToRight ProfileLayoutEnforcement.applyForcedLTR(to: scrollView) scrollView.hasVerticalScroller = true scrollView.hasHorizontalScroller = false scrollView.autohidesScrollers = true scrollView.drawsBackground = false scrollView.borderType = .noBorder scrollView.scrollerStyle = .overlay scrollView.automaticallyAdjustsContentInsets = false scrollView.contentView.userInterfaceLayoutDirection = .leftToRight ProfileLayoutEnforcement.applyForcedLTR(to: scrollView.contentView) if #available(macOS 10.11, *) { scrollView.horizontalScrollElasticity = .none } documentView.translatesAutoresizingMaskIntoConstraints = false documentView.userInterfaceLayoutDirection = .leftToRight ProfileLayoutEnforcement.applyForcedLTR(to: documentView) cardView.translatesAutoresizingMaskIntoConstraints = false cardView.wantsLayer = true cardView.layer?.backgroundColor = ProfilePagePalette.cardBackground.cgColor cardView.layer?.cornerRadius = 18 cardView.layer?.borderWidth = 1 cardView.layer?.borderColor = ProfilePagePalette.border.cgColor cardView.layer?.masksToBounds = false cardView.layer?.shadowColor = NSColor.black.cgColor cardView.layer?.shadowOpacity = 0.06 cardView.layer?.shadowRadius = 20 cardView.layer?.shadowOffset = CGSize(width: 0, height: 10) cardView.userInterfaceLayoutDirection = .leftToRight ProfileLayoutEnforcement.applyForcedLTR(to: cardView) if #available(macOS 11.0, *) { cardView.layer?.cornerCurve = .continuous } formStack.translatesAutoresizingMaskIntoConstraints = false formStack.orientation = .vertical formStack.alignment = .leading formStack.distribution = .fill formStack.spacing = 24 formStack.edgeInsets = NSEdgeInsets(top: 32, left: 0, bottom: 32, right: 0) formStack.userInterfaceLayoutDirection = .leftToRight ProfileLayoutEnforcement.applyForcedLTR(to: formStack) formStack.setContentHuggingPriority(.defaultLow, for: .horizontal) formStack.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) topChrome.translatesAutoresizingMaskIntoConstraints = false topChrome.userInterfaceLayoutDirection = .leftToRight backButton.translatesAutoresizingMaskIntoConstraints = false backButton.bezelStyle = .rounded backButton.isBordered = false backButton.font = .systemFont(ofSize: 13, weight: .medium) backButton.contentTintColor = ProfilePagePalette.brandBlue backButton.target = self backButton.action = #selector(didTapBack) contextLabel.translatesAutoresizingMaskIntoConstraints = false contextLabel.font = .systemFont(ofSize: 15, weight: .semibold) contextLabel.textColor = ProfilePagePalette.primaryText contextLabel.stringValue = L("New profile") contextLabel.backgroundColor = .clear contextLabel.isBordered = false contextLabel.isEditable = false contextLabel.isSelectable = false ProfileLayoutEnforcement.applyLeftAlignedTextField(contextLabel) topChrome.addSubview(backButton) topChrome.addSubview(contextLabel) NSLayoutConstraint.activate([ backButton.leadingAnchor.constraint(equalTo: topChrome.leadingAnchor, constant: 4), backButton.centerYAnchor.constraint(equalTo: topChrome.centerYAnchor), contextLabel.leadingAnchor.constraint(equalTo: backButton.trailingAnchor, constant: 10), contextLabel.centerYAnchor.constraint(equalTo: topChrome.centerYAnchor), contextLabel.trailingAnchor.constraint(lessThanOrEqualTo: topChrome.trailingAnchor, constant: -8) ]) addSubview(topChrome) addSubview(scrollView) scrollView.documentView = documentView documentView.addSubview(cardView) cardView.addSubview(formStack) NSLayoutConstraint.activate([ topChrome.leftAnchor.constraint(equalTo: leftAnchor), topChrome.rightAnchor.constraint(equalTo: rightAnchor), topChrome.topAnchor.constraint(equalTo: topAnchor, constant: 4), topChrome.heightAnchor.constraint(equalToConstant: 40), scrollView.leftAnchor.constraint(equalTo: leftAnchor), scrollView.rightAnchor.constraint(equalTo: rightAnchor), scrollView.topAnchor.constraint(equalTo: topChrome.bottomAnchor, constant: 2), scrollView.bottomAnchor.constraint(equalTo: bottomAnchor), // Pin the document to the clip view’s geometric width so LTR/RTL semantics cannot slide the form. documentView.leftAnchor.constraint(equalTo: scrollView.contentView.leftAnchor), documentView.rightAnchor.constraint(equalTo: scrollView.contentView.rightAnchor), documentView.topAnchor.constraint(equalTo: scrollView.contentView.topAnchor), documentView.bottomAnchor.constraint(equalTo: cardView.bottomAnchor, constant: Self.horizontalPageInset), // Pin both edges so the card always spans the clip width minus insets; a separate width equal to a large constant can conflict with the clip and slide the card to the trailing edge. cardView.leftAnchor.constraint(equalTo: documentView.leftAnchor, constant: Self.horizontalPageInset), cardView.rightAnchor.constraint(equalTo: documentView.rightAnchor, constant: -Self.horizontalPageInset), cardView.topAnchor.constraint(equalTo: documentView.topAnchor, constant: Self.horizontalPageInset), formStack.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: Self.cardContentHorizontalInset), formStack.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -Self.cardContentHorizontalInset), formStack.topAnchor.constraint(equalTo: cardView.topAnchor), formStack.bottomAnchor.constraint(equalTo: cardView.bottomAnchor) ]) addFullWidthArrangedSubview( labeledGroup(labelKey: "Profile Name *", field: profileNameField, placeholderKey: "Marketing Director Profile") ) addFullWidthArrangedSubview(sectionHeading("Personal Information")) let nameGroup = labeledGroup(labelKey: "Full Name *", field: fullNameField, placeholderKey: "John Doe") let emailGroup = labeledGroup(labelKey: "Email *", field: emailField, placeholderKey: "john@example.com") nameEmailRow = ProfileDualFieldRow(left: nameGroup, right: emailGroup, spacing: 12) addFullWidthArrangedSubview(nameEmailRow) let phoneGroup = labeledGroup(labelKey: "Phone", field: phoneField, placeholderKey: "+1 (555) 123-4567") let jobGroup = labeledGroup(labelKey: "Job Title *", field: jobTitleField, placeholderKey: "Software Engineer") phoneJobRow = ProfileDualFieldRow(left: phoneGroup, right: jobGroup, spacing: 12) addFullWidthArrangedSubview(phoneJobRow) addFullWidthArrangedSubview( labeledGroup(labelKey: "Address", field: addressField, placeholderKey: "123 Main St, City, State, ZIP") ) addFullWidthArrangedSubview(careerSummaryBlock()) addFullWidthArrangedSubview(horizontalSeparator()) addFullWidthArrangedSubview(workExperienceSection()) addFullWidthArrangedSubview(horizontalSeparator()) addFullWidthArrangedSubview(educationSection()) addFullWidthArrangedSubview(horizontalSeparator()) addFullWidthArrangedSubview( multilineProfileBlock( labelKey: "Certificates / Rewards", placeholderKey: "List your certificates and awards...", field: certificatesField, minHeight: 100 ) ) addFullWidthArrangedSubview(horizontalSeparator()) addFullWidthArrangedSubview( multilineProfileBlock( labelKey: "Interests", placeholderKey: "List your interests and hobbies...", field: interestsField, minHeight: 100 ) ) addFullWidthArrangedSubview(horizontalSeparator()) addFullWidthArrangedSubview( multilineProfileBlock( labelKey: "Languages", placeholderKey: "List languages you speak (e.g., English - Native, Spanish - Fluent)...", field: languagesField, minHeight: 100 ) ) addFullWidthArrangedSubview(horizontalSeparator()) addFullWidthArrangedSubview(referralBlock()) addFullWidthArrangedSubview(saveButtonHost()) saveButton.target = self saveButton.action = #selector(didTapSave) appendWorkExperienceEntry() appendEducationEntry() ProfileLayoutEnforcement.applyForcedLTRSubtree(from: self) } func prepareNewProfile() { editingProfileID = nil applyLocalizedStrings() contextLabel.stringValue = L("New profile") applyForm( from: SavedProfile( id: UUID(), profileDisplayName: "", personal: .empty, careerSummary: "", workExperiences: [.empty], educations: [.empty], certificates: "", interests: "", languages: "", referral: "" ) ) } func loadSavedProfile(_ profile: SavedProfile) { editingProfileID = profile.id applyLocalizedStrings() contextLabel.stringValue = L("Edit profile") applyForm(from: profile) } private func applyForm(from profile: SavedProfile) { profileNameField.stringValue = profile.profileDisplayName applyPersonalInformation(profile.personal) careerField.stringValue = profile.careerSummary certificatesField.stringValue = profile.certificates interestsField.stringValue = profile.interests languagesField.stringValue = profile.languages referralField.stringValue = profile.referral let workCount = max(1, profile.workExperiences.count) syncWorkExperienceRowCount(to: workCount) if profile.workExperiences.isEmpty { workExperienceEntries[0].applyPayload(.empty) } else { for (i, payload) in profile.workExperiences.enumerated() where i < workExperienceEntries.count { workExperienceEntries[i].applyPayload(payload) } } let eduCount = max(1, profile.educations.count) syncEducationRowCount(to: eduCount) if profile.educations.isEmpty { educationEntries[0].applyPayload(.empty) } else { for (i, payload) in profile.educations.enumerated() where i < educationEntries.count { educationEntries[i].applyPayload(payload) } } lastCompactLayout = nil needsLayout = true } private func syncWorkExperienceRowCount(to target: Int) { let n = max(1, target) while workExperienceEntries.count < n { appendWorkExperienceEntry() } while workExperienceEntries.count > n { guard let last = workExperienceEntries.last, workExperienceEntries.count > 1 else { break } removeWorkExperienceEntry(last) } } private func syncEducationRowCount(to target: Int) { let n = max(1, target) while educationEntries.count < n { appendEducationEntry() } while educationEntries.count > n { guard let last = educationEntries.last, educationEntries.count > 1 else { break } removeEducationEntry(last) } } private func captureSavedProfileForSave() -> SavedProfile { let id = editingProfileID ?? UUID() return SavedProfile( id: id, profileDisplayName: profileNameField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines), personal: collectPersonalInformationFromFields(), careerSummary: careerField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines), workExperiences: workExperienceEntries.map { $0.capturePayload() }, educations: educationEntries.map { $0.capturePayload() }, certificates: certificatesField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines), interests: interestsField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines), languages: languagesField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines), referral: referralField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) ) } @objc private func didTapBack() { onDismiss?() } private func applyPersonalInformation(_ info: PersonalInformation) { fullNameField.stringValue = info.fullName emailField.stringValue = info.email phoneField.stringValue = info.phone jobTitleField.stringValue = info.jobTitle addressField.stringValue = info.address } private func collectPersonalInformationFromFields() -> PersonalInformation { PersonalInformation( fullName: fullNameField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines), email: emailField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines), phone: phoneField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines), jobTitle: jobTitleField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines), address: addressField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) ) } private func applyResponsiveRowsIfNeeded() { let w = cardView.bounds.width guard w > 1 else { return } let formWidth = max(0, w - 2 * Self.cardContentHorizontalInset - formStack.edgeInsets.left - formStack.edgeInsets.right) let compact = formWidth < Self.compactFormWidth guard compact != lastCompactLayout else { return } lastCompactLayout = compact nameEmailRow.setCompact(compact) phoneJobRow.setCompact(compact) for entry in workExperienceEntries { entry.applyCompactLayout(compact) } for entry in educationEntries { entry.applyCompactLayout(compact) } } private func addFullWidthArrangedSubview(_ view: NSView) { formStack.addArrangedSubview(view) view.setContentHuggingPriority(.defaultLow, for: .horizontal) view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) view.widthAnchor.constraint(equalTo: formStack.widthAnchor).isActive = true } private func sectionHeading(_ key: String) -> NSView { let label = NSTextField(labelWithString: L(key)) ProfileFormLocalization.tagSection(label, key: key) label.font = .systemFont(ofSize: 15, weight: .semibold) label.textColor = ProfilePagePalette.primaryText label.baseWritingDirection = .leftToRight label.translatesAutoresizingMaskIntoConstraints = false label.setContentHuggingPriority(.defaultHigh, for: .horizontal) ProfileLayoutEnforcement.applyLeftAlignedTextField(label) let spacer = NSView() spacer.translatesAutoresizingMaskIntoConstraints = false spacer.setContentHuggingPriority(.defaultLow, for: .horizontal) let row = NSStackView(views: [label, spacer]) row.orientation = .horizontal row.alignment = .centerY row.distribution = .fill row.spacing = 0 row.userInterfaceLayoutDirection = .leftToRight ProfileLayoutEnforcement.applyForcedLTR(to: row) row.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ label.leftAnchor.constraint(equalTo: row.leftAnchor) ]) return row } private func labeledGroup(labelKey: String, field: NSTextField, placeholderKey: String) -> NSView { let label = NSTextField(labelWithString: L(labelKey)) ProfileFormLocalization.tagLabel(label, key: labelKey) label.font = .systemFont(ofSize: 12, weight: .medium) label.textColor = ProfilePagePalette.secondaryText label.translatesAutoresizingMaskIntoConstraints = false label.setContentHuggingPriority(.defaultLow, for: .horizontal) ProfileLayoutEnforcement.applyLeftAlignedTextField(label) styleSingleLineField(field, placeholderKey: placeholderKey) let wrap = roundedFieldChrome(containing: field, minHeight: 40) let stack = NSStackView(views: [label, wrap]) stack.orientation = .vertical stack.spacing = 8 // `.leading` keeps rows on the geometric left; explicit widths keep labels/fields full-width (`.width` alone can still hug the trailing edge under RTL-style layout). stack.alignment = .leading stack.translatesAutoresizingMaskIntoConstraints = false stack.userInterfaceLayoutDirection = .leftToRight ProfileLayoutEnforcement.applyForcedLTR(to: stack) stack.setContentHuggingPriority(.defaultLow, for: .horizontal) wrap.setContentHuggingPriority(.defaultLow, for: .horizontal) wrap.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) NSLayoutConstraint.activate([ label.leftAnchor.constraint(equalTo: stack.leftAnchor), label.widthAnchor.constraint(equalTo: stack.widthAnchor), wrap.widthAnchor.constraint(equalTo: stack.widthAnchor) ]) return stack } private func styleSingleLineField(_ field: NSTextField, placeholderKey: String) { ProfileFormLocalization.tagPlaceholder(field, key: placeholderKey) field.translatesAutoresizingMaskIntoConstraints = false field.isBordered = false field.drawsBackground = false field.focusRingType = .none field.font = .systemFont(ofSize: 14, weight: .regular) field.textColor = ProfilePagePalette.primaryText field.setContentHuggingPriority(.defaultLow, for: .horizontal) field.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) ProfileFormLocalization.applyPlaceholder(L(placeholderKey), to: field) field.cell?.usesSingleLineMode = true field.cell?.wraps = false field.cell?.isScrollable = true ProfileLayoutEnforcement.applyLeftAlignedTextField(field) } private func roundedFieldChrome(containing field: NSTextField, minHeight: CGFloat) -> NSView { let wrap = NSView() wrap.translatesAutoresizingMaskIntoConstraints = false wrap.wantsLayer = true wrap.layer?.backgroundColor = ProfilePagePalette.fieldFill.cgColor wrap.layer?.cornerRadius = 10 wrap.layer?.borderWidth = 1 wrap.layer?.borderColor = ProfilePagePalette.border.cgColor if #available(macOS 11.0, *) { wrap.layer?.cornerCurve = .continuous } wrap.addSubview(field) ProfileLayoutEnforcement.applyForcedLTR(to: wrap) NSLayoutConstraint.activate([ field.leftAnchor.constraint(equalTo: wrap.leftAnchor, constant: 12), field.rightAnchor.constraint(equalTo: wrap.rightAnchor, constant: -12), field.centerYAnchor.constraint(equalTo: wrap.centerYAnchor), wrap.heightAnchor.constraint(greaterThanOrEqualToConstant: minHeight) ]) return wrap } private func careerSummaryBlock() -> NSView { let label = NSTextField(labelWithString: L("Career Summary")) ProfileFormLocalization.tagLabel(label, key: "Career Summary") label.font = .systemFont(ofSize: 12, weight: .medium) label.textColor = ProfilePagePalette.secondaryText label.translatesAutoresizingMaskIntoConstraints = false ProfileLayoutEnforcement.applyLeftAlignedTextField(label) careerField.translatesAutoresizingMaskIntoConstraints = false careerField.isEditable = true careerField.isSelectable = true careerField.isBordered = false careerField.drawsBackground = false careerField.focusRingType = .none careerField.font = .systemFont(ofSize: 14, weight: .regular) careerField.textColor = ProfilePagePalette.primaryText careerField.maximumNumberOfLines = 0 careerField.cell?.wraps = true careerField.cell?.isScrollable = false careerField.cell?.usesSingleLineMode = false careerField.stringValue = "" careerField.setContentHuggingPriority(.defaultLow, for: .horizontal) careerField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) ProfileFormLocalization.tagPlaceholder( careerField, key: "Brief overview of your professional background and key achievements..." ) ProfileFormLocalization.applyPlaceholder( L("Brief overview of your professional background and key achievements..."), to: careerField ) ProfileLayoutEnforcement.applyLeftAlignedTextField(careerField) let wrap = NSView() wrap.translatesAutoresizingMaskIntoConstraints = false wrap.wantsLayer = true wrap.layer?.backgroundColor = ProfilePagePalette.fieldFill.cgColor wrap.layer?.cornerRadius = 10 wrap.layer?.borderWidth = 1 wrap.layer?.borderColor = ProfilePagePalette.border.cgColor if #available(macOS 11.0, *) { wrap.layer?.cornerCurve = .continuous } wrap.addSubview(careerField) ProfileLayoutEnforcement.applyForcedLTR(to: wrap) NSLayoutConstraint.activate([ careerField.leftAnchor.constraint(equalTo: wrap.leftAnchor, constant: 12), careerField.rightAnchor.constraint(equalTo: wrap.rightAnchor, constant: -12), careerField.topAnchor.constraint(equalTo: wrap.topAnchor, constant: 10), careerField.bottomAnchor.constraint(equalTo: wrap.bottomAnchor, constant: -10), wrap.heightAnchor.constraint(greaterThanOrEqualToConstant: 168) ]) let stack = NSStackView(views: [label, wrap]) stack.orientation = .vertical stack.spacing = 8 stack.alignment = .leading stack.translatesAutoresizingMaskIntoConstraints = false stack.userInterfaceLayoutDirection = .leftToRight ProfileLayoutEnforcement.applyForcedLTR(to: stack) stack.setContentHuggingPriority(.defaultLow, for: .horizontal) wrap.setContentHuggingPriority(.defaultLow, for: .horizontal) NSLayoutConstraint.activate([ label.leftAnchor.constraint(equalTo: stack.leftAnchor), label.widthAnchor.constraint(equalTo: stack.widthAnchor), wrap.widthAnchor.constraint(equalTo: stack.widthAnchor) ]) return stack } private func multilineProfileBlock(labelKey: String, placeholderKey: String, field: NSTextField, minHeight: CGFloat) -> NSView { let label = NSTextField(labelWithString: L(labelKey)) ProfileFormLocalization.tagLabel(label, key: labelKey) label.font = .systemFont(ofSize: 12, weight: .medium) label.textColor = ProfilePagePalette.secondaryText label.translatesAutoresizingMaskIntoConstraints = false ProfileLayoutEnforcement.applyLeftAlignedTextField(label) ProfileFormLocalization.tagPlaceholder(field, key: placeholderKey) field.translatesAutoresizingMaskIntoConstraints = false field.isEditable = true field.isSelectable = true field.isBordered = false field.drawsBackground = false field.focusRingType = .none field.font = .systemFont(ofSize: 14, weight: .regular) field.textColor = ProfilePagePalette.primaryText field.maximumNumberOfLines = 0 field.cell?.wraps = true field.cell?.isScrollable = false field.cell?.usesSingleLineMode = false field.stringValue = "" field.setContentHuggingPriority(.defaultLow, for: .horizontal) field.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) ProfileFormLocalization.applyPlaceholder(L(placeholderKey), to: field) ProfileLayoutEnforcement.applyLeftAlignedTextField(field) let wrap = NSView() wrap.translatesAutoresizingMaskIntoConstraints = false wrap.wantsLayer = true wrap.layer?.backgroundColor = ProfilePagePalette.fieldFill.cgColor wrap.layer?.cornerRadius = 10 wrap.layer?.borderWidth = 1 wrap.layer?.borderColor = ProfilePagePalette.border.cgColor if #available(macOS 11.0, *) { wrap.layer?.cornerCurve = .continuous } wrap.addSubview(field) ProfileLayoutEnforcement.applyForcedLTR(to: wrap) NSLayoutConstraint.activate([ field.leftAnchor.constraint(equalTo: wrap.leftAnchor, constant: 12), field.rightAnchor.constraint(equalTo: wrap.rightAnchor, constant: -12), field.topAnchor.constraint(equalTo: wrap.topAnchor, constant: 10), field.bottomAnchor.constraint(equalTo: wrap.bottomAnchor, constant: -10), wrap.heightAnchor.constraint(greaterThanOrEqualToConstant: minHeight) ]) let stack = NSStackView(views: [label, wrap]) stack.orientation = .vertical stack.spacing = 8 stack.alignment = .leading stack.translatesAutoresizingMaskIntoConstraints = false stack.userInterfaceLayoutDirection = .leftToRight ProfileLayoutEnforcement.applyForcedLTR(to: stack) stack.setContentHuggingPriority(.defaultLow, for: .horizontal) wrap.setContentHuggingPriority(.defaultLow, for: .horizontal) NSLayoutConstraint.activate([ label.leftAnchor.constraint(equalTo: stack.leftAnchor), label.widthAnchor.constraint(equalTo: stack.widthAnchor), wrap.widthAnchor.constraint(equalTo: stack.widthAnchor) ]) return stack } private func referralBlock() -> NSView { let label = NSTextField(labelWithString: L("Referral (Optional)")) ProfileFormLocalization.tagLabel(label, key: "Referral (Optional)") label.font = .systemFont(ofSize: 12, weight: .medium) label.textColor = ProfilePagePalette.secondaryText label.translatesAutoresizingMaskIntoConstraints = false ProfileLayoutEnforcement.applyLeftAlignedTextField(label) styleSingleLineField(referralField, placeholderKey: "Referred by (Company/Person Name)") let wrap = roundedFieldChrome(containing: referralField, minHeight: 40) let helper = NSTextField(wrappingLabelWithString: L("If someone referred you for this job, enter their name or company here")) helper.font = .systemFont(ofSize: 11, weight: .regular) helper.textColor = ProfilePagePalette.secondaryText helper.translatesAutoresizingMaskIntoConstraints = false ProfileLayoutEnforcement.applyLeftAlignedTextField(helper) referralHelperLabel = helper let stack = NSStackView(views: [label, wrap, helper]) stack.orientation = .vertical stack.spacing = 8 stack.alignment = .leading stack.translatesAutoresizingMaskIntoConstraints = false stack.userInterfaceLayoutDirection = .leftToRight ProfileLayoutEnforcement.applyForcedLTR(to: stack) stack.setContentHuggingPriority(.defaultLow, for: .horizontal) wrap.setContentHuggingPriority(.defaultLow, for: .horizontal) if #available(macOS 10.11, *) { stack.setCustomSpacing(6, after: wrap) } NSLayoutConstraint.activate([ label.leftAnchor.constraint(equalTo: stack.leftAnchor), label.widthAnchor.constraint(equalTo: stack.widthAnchor), wrap.widthAnchor.constraint(equalTo: stack.widthAnchor), helper.leftAnchor.constraint(equalTo: stack.leftAnchor), helper.widthAnchor.constraint(equalTo: stack.widthAnchor) ]) return stack } private func horizontalSeparator() -> NSView { let box = NSBox() box.boxType = .separator box.translatesAutoresizingMaskIntoConstraints = false return box } private func workExperienceSection() -> NSView { let title = NSTextField(labelWithString: L("Work Experience")) ProfileFormLocalization.tagSection(title, key: "Work Experience") title.font = .systemFont(ofSize: 15, weight: .semibold) title.textColor = ProfilePagePalette.primaryText title.translatesAutoresizingMaskIntoConstraints = false title.setContentHuggingPriority(.defaultLow, for: .horizontal) ProfileLayoutEnforcement.applyLeftAlignedTextField(title) let addButton = NSButton(title: L("+ Add Another"), target: self, action: #selector(didTapAddWorkExperience)) ProfileFormLocalization.tagButton(addButton, key: "+ Add Another") addButton.translatesAutoresizingMaskIntoConstraints = false addButton.bezelStyle = .rounded addButton.isBordered = true addButton.font = .systemFont(ofSize: 12, weight: .medium) addButton.controlSize = .regular addButton.setContentHuggingPriority(.required, for: .horizontal) let spacer = NSView() spacer.translatesAutoresizingMaskIntoConstraints = false spacer.setContentHuggingPriority(.defaultLow, for: .horizontal) let headerRow = NSStackView(views: [title, spacer, addButton]) headerRow.orientation = .horizontal headerRow.alignment = .centerY headerRow.distribution = .fill headerRow.spacing = 12 headerRow.userInterfaceLayoutDirection = .leftToRight ProfileLayoutEnforcement.applyForcedLTR(to: headerRow) headerRow.translatesAutoresizingMaskIntoConstraints = false workExperienceRowsStack.translatesAutoresizingMaskIntoConstraints = false workExperienceRowsStack.orientation = .vertical workExperienceRowsStack.spacing = 20 workExperienceRowsStack.alignment = .leading workExperienceRowsStack.userInterfaceLayoutDirection = .leftToRight ProfileLayoutEnforcement.applyForcedLTR(to: workExperienceRowsStack) let outer = NSStackView(views: [headerRow, workExperienceRowsStack]) outer.orientation = .vertical outer.spacing = 16 outer.alignment = .leading outer.translatesAutoresizingMaskIntoConstraints = false outer.userInterfaceLayoutDirection = .leftToRight ProfileLayoutEnforcement.applyForcedLTR(to: outer) outer.pinAllArrangedSubviewWidthsEqualToStackWidth() return outer } private func educationSection() -> NSView { let title = NSTextField(labelWithString: L("Education")) ProfileFormLocalization.tagSection(title, key: "Education") title.font = .systemFont(ofSize: 15, weight: .semibold) title.textColor = ProfilePagePalette.primaryText title.translatesAutoresizingMaskIntoConstraints = false title.setContentHuggingPriority(.defaultLow, for: .horizontal) ProfileLayoutEnforcement.applyLeftAlignedTextField(title) let addButton = NSButton(title: L("+ Add Another"), target: self, action: #selector(didTapAddEducation)) ProfileFormLocalization.tagButton(addButton, key: "+ Add Another") addButton.translatesAutoresizingMaskIntoConstraints = false addButton.bezelStyle = .rounded addButton.isBordered = true addButton.font = .systemFont(ofSize: 12, weight: .medium) addButton.setContentHuggingPriority(.required, for: .horizontal) let spacer = NSView() spacer.translatesAutoresizingMaskIntoConstraints = false spacer.setContentHuggingPriority(.defaultLow, for: .horizontal) let headerRow = NSStackView(views: [title, spacer, addButton]) headerRow.orientation = .horizontal headerRow.alignment = .centerY headerRow.distribution = .fill headerRow.spacing = 12 headerRow.userInterfaceLayoutDirection = .leftToRight ProfileLayoutEnforcement.applyForcedLTR(to: headerRow) headerRow.translatesAutoresizingMaskIntoConstraints = false educationRowsStack.translatesAutoresizingMaskIntoConstraints = false educationRowsStack.orientation = .vertical educationRowsStack.spacing = 16 educationRowsStack.alignment = .leading educationRowsStack.userInterfaceLayoutDirection = .leftToRight ProfileLayoutEnforcement.applyForcedLTR(to: educationRowsStack) let outer = NSStackView(views: [headerRow, educationRowsStack]) outer.orientation = .vertical outer.spacing = 16 outer.alignment = .leading outer.translatesAutoresizingMaskIntoConstraints = false outer.userInterfaceLayoutDirection = .leftToRight ProfileLayoutEnforcement.applyForcedLTR(to: outer) outer.pinAllArrangedSubviewWidthsEqualToStackWidth() return outer } private func appendWorkExperienceEntry() { let entry = WorkExperienceEntryView() entry.translatesAutoresizingMaskIntoConstraints = false if let compact = lastCompactLayout { entry.applyCompactLayout(compact) } entry.onDelete = { [weak self, weak entry] in guard let self, let entry else { return } self.removeWorkExperienceEntry(entry) } workExperienceEntries.append(entry) workExperienceRowsStack.addArrangedSubview(entry) entry.widthAnchor.constraint(equalTo: workExperienceRowsStack.widthAnchor).isActive = true renumberWorkExperienceEntries() refreshWorkExperienceDeleteButtons() } private func removeWorkExperienceEntry(_ entry: WorkExperienceEntryView) { guard workExperienceEntries.count > 1 else { return } workExperienceEntries.removeAll { $0 === entry } workExperienceRowsStack.removeArrangedSubview(entry) entry.removeFromSuperview() renumberWorkExperienceEntries() refreshWorkExperienceDeleteButtons() } private func renumberWorkExperienceEntries() { for (i, entry) in workExperienceEntries.enumerated() { entry.setExperienceIndex(i + 1) } } private func refreshWorkExperienceDeleteButtons() { let hide = workExperienceEntries.count <= 1 for entry in workExperienceEntries { entry.setDeleteHidden(hide) } } private func appendEducationEntry() { let entry = EducationEntryView() entry.translatesAutoresizingMaskIntoConstraints = false if let compact = lastCompactLayout { entry.applyCompactLayout(compact) } entry.onDelete = { [weak self, weak entry] in guard let self, let entry else { return } self.removeEducationEntry(entry) } educationEntries.append(entry) educationRowsStack.addArrangedSubview(entry) entry.widthAnchor.constraint(equalTo: educationRowsStack.widthAnchor).isActive = true renumberEducationEntries() refreshEducationDeleteButtons() } private func removeEducationEntry(_ entry: EducationEntryView) { guard educationEntries.count > 1 else { return } educationEntries.removeAll { $0 === entry } educationRowsStack.removeArrangedSubview(entry) entry.removeFromSuperview() renumberEducationEntries() refreshEducationDeleteButtons() } private func renumberEducationEntries() { for (i, entry) in educationEntries.enumerated() { entry.setEducationIndex(i + 1) } } private func refreshEducationDeleteButtons() { let hide = educationEntries.count <= 1 for entry in educationEntries { entry.setDeleteHidden(hide) } } @objc private func didTapAddWorkExperience() { appendWorkExperienceEntry() } @objc private func didTapAddEducation() { appendEducationEntry() } private func saveButtonHost() -> NSView { saveButton.translatesAutoresizingMaskIntoConstraints = false saveButton.setContentHuggingPriority(.defaultLow, for: .horizontal) let host = NSView() host.translatesAutoresizingMaskIntoConstraints = false host.userInterfaceLayoutDirection = .leftToRight ProfileLayoutEnforcement.applyForcedLTR(to: host) host.addSubview(saveButton) let preferredWidth = saveButton.widthAnchor.constraint(equalToConstant: 292) preferredWidth.priority = .defaultHigh NSLayoutConstraint.activate([ saveButton.centerXAnchor.constraint(equalTo: host.centerXAnchor), saveButton.topAnchor.constraint(equalTo: host.topAnchor), saveButton.bottomAnchor.constraint(equalTo: host.bottomAnchor), saveButton.heightAnchor.constraint(equalToConstant: 54), preferredWidth, saveButton.widthAnchor.constraint(lessThanOrEqualTo: host.widthAnchor) ]) return host } @objc private func didTapSave() { window?.makeFirstResponder(nil) let profile = captureSavedProfileForSave() var missing: [String] = [] if profile.profileDisplayName.isEmpty { missing.append(L("Profile name")) } if profile.personal.fullName.isEmpty { missing.append(L("Full Name")) } if profile.personal.email.isEmpty { missing.append(L("Email")) } if profile.personal.jobTitle.isEmpty { missing.append(L("Job Title")) } guard missing.isEmpty else { let alert = NSAlert() alert.messageText = L("Complete required fields") alert.informativeText = String(format: L("Please fill in: %@."), missing.joined(separator: ", ")) alert.alertStyle = .informational alert.addButton(withTitle: L("OK")) if let window = window { alert.beginSheetModal(for: window) { _ in } } else { alert.runModal() } return } SavedProfilesStore.upsert(profile) editingProfileID = profile.id onDismiss?() } } // MARK: - Work experience & education rows private enum ProfileEntryCardLayout { /// Horizontal inset of fields inside each work/education entry card (matches main profile form). static let horizontalInset: CGFloat = 28 } private final class WorkExperienceEntryView: NSView { var onDelete: (() -> Void)? private let subtitleLabel = NSTextField(labelWithString: String(format: L("Experience %d"), 1)) private let deleteButton = NSButton() private let jobTitleField = NSTextField() private let companyField = NSTextField() private let durationField = NSTextField() private let descriptionField = NSTextField() private var jobCompanyRow: ProfileDualFieldRow! override init(frame frameRect: NSRect) { super.init(frame: frameRect) configure() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func setExperienceIndex(_ index: Int) { subtitleLabel.stringValue = String(format: L("Experience %d"), index) } func setDeleteHidden(_ hidden: Bool) { deleteButton.isHidden = hidden } func applyCompactLayout(_ compact: Bool) { jobCompanyRow.setCompact(compact) } func capturePayload() -> WorkExperiencePayload { WorkExperiencePayload( jobTitle: jobTitleField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines), company: companyField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines), duration: durationField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines), description: descriptionField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) ) } func applyPayload(_ payload: WorkExperiencePayload) { jobTitleField.stringValue = payload.jobTitle companyField.stringValue = payload.company durationField.stringValue = payload.duration descriptionField.stringValue = payload.description } private func configure() { userInterfaceLayoutDirection = .leftToRight ProfileLayoutEnforcement.applyForcedLTR(to: self) wantsLayer = true layer?.cornerRadius = 14 layer?.borderWidth = 1 layer?.borderColor = ProfilePagePalette.border.cgColor layer?.backgroundColor = ProfilePagePalette.cardBackground.cgColor layer?.masksToBounds = false layer?.shadowColor = NSColor.black.cgColor layer?.shadowOpacity = 0.05 layer?.shadowRadius = 12 layer?.shadowOffset = CGSize(width: 0, height: 6) if #available(macOS 11.0, *) { layer?.cornerCurve = .continuous } subtitleLabel.font = .systemFont(ofSize: 12, weight: .medium) subtitleLabel.textColor = ProfilePagePalette.secondaryText subtitleLabel.translatesAutoresizingMaskIntoConstraints = false ProfileLayoutEnforcement.applyLeftAlignedTextField(subtitleLabel) deleteButton.translatesAutoresizingMaskIntoConstraints = false deleteButton.isBordered = false deleteButton.bezelStyle = .regularSquare deleteButton.focusRingType = .none deleteButton.contentTintColor = ProfilePagePalette.destructive deleteButton.target = self deleteButton.action = #selector(didTapDelete) if #available(macOS 11.0, *) { deleteButton.image = NSImage(systemSymbolName: "trash", accessibilityDescription: L("Remove experience")) deleteButton.imagePosition = .imageOnly ProfileFormLocalization.tagButton(deleteButton, key: "Remove experience") } else { deleteButton.title = L("Remove") deleteButton.font = .systemFont(ofSize: 12, weight: .medium) ProfileFormLocalization.tagButton(deleteButton, key: "Remove") } let headerSpacer = NSView() headerSpacer.translatesAutoresizingMaskIntoConstraints = false headerSpacer.setContentHuggingPriority(.defaultLow, for: .horizontal) let headerRow = NSStackView(views: [subtitleLabel, headerSpacer, deleteButton]) headerRow.orientation = .horizontal headerRow.alignment = .centerY headerRow.distribution = .fill headerRow.spacing = 8 headerRow.userInterfaceLayoutDirection = .leftToRight ProfileLayoutEnforcement.applyForcedLTR(to: headerRow) headerRow.translatesAutoresizingMaskIntoConstraints = false let jobGroup = Self.labeledFieldStack( labelKey: "Job Title *", field: jobTitleField, placeholderKey: "e.g., Software Engineer" ) let companyGroup = Self.labeledFieldStack( labelKey: "Company Name *", field: companyField, placeholderKey: "e.g., Google" ) jobCompanyRow = ProfileDualFieldRow(left: jobGroup, right: companyGroup, spacing: 12) let durationGroup = Self.labeledFieldStack( labelKey: "Duration *", field: durationField, placeholderKey: "e.g., Jan 2020 - Present" ) let descriptionGroup = Self.multilineLabeledStack( labelKey: "Description", field: descriptionField, placeholderKey: "Describe your responsibilities and achievements...", minHeight: 120 ) let inner = NSStackView(views: [headerRow, jobCompanyRow, durationGroup, descriptionGroup]) inner.orientation = .vertical inner.spacing = 16 inner.alignment = .leading inner.translatesAutoresizingMaskIntoConstraints = false inner.edgeInsets = NSEdgeInsets() inner.userInterfaceLayoutDirection = .leftToRight ProfileLayoutEnforcement.applyForcedLTR(to: inner) inner.pinAllArrangedSubviewWidthsEqualToStackWidth() addSubview(inner) NSLayoutConstraint.activate([ inner.leadingAnchor.constraint(equalTo: leadingAnchor, constant: ProfileEntryCardLayout.horizontalInset), inner.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -ProfileEntryCardLayout.horizontalInset), inner.topAnchor.constraint(equalTo: topAnchor, constant: 16), inner.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -16) ]) } override func layout() { super.layout() for field in [jobTitleField, companyField, durationField, descriptionField] { if let wrap = field.superview, wrap.bounds.width > 2 { let w = max(1, wrap.bounds.width - 24) if abs(field.preferredMaxLayoutWidth - w) > 0.5 { field.preferredMaxLayoutWidth = w } } } guard let layer = layer, layer.shadowOpacity > 0 else { return } let r = layer.cornerRadius layer.shadowPath = CGPath(roundedRect: bounds, cornerWidth: r, cornerHeight: r, transform: nil) } @objc private func didTapDelete() { onDelete?() } func applyCurrentAppearance() { layer?.backgroundColor = ProfilePagePalette.cardBackground.cgColor layer?.borderColor = ProfilePagePalette.border.cgColor subtitleLabel.textColor = ProfilePagePalette.secondaryText deleteButton.contentTintColor = ProfilePagePalette.destructive ProfileThemeAppearance.refreshFormSubtree(self) } fileprivate static func labeledFieldStack(labelKey: String, field: NSTextField, placeholderKey: String) -> NSView { let label = NSTextField(labelWithString: L(labelKey)) ProfileFormLocalization.tagLabel(label, key: labelKey) label.font = .systemFont(ofSize: 12, weight: .medium) label.textColor = ProfilePagePalette.secondaryText label.translatesAutoresizingMaskIntoConstraints = false ProfileLayoutEnforcement.applyLeftAlignedTextField(label) styleSingleLineField(field, placeholderKey: placeholderKey) let wrap = roundedChrome(around: field, minHeight: 40) let stack = NSStackView(views: [label, wrap]) stack.orientation = .vertical stack.spacing = 8 stack.alignment = .leading stack.translatesAutoresizingMaskIntoConstraints = false stack.userInterfaceLayoutDirection = .leftToRight ProfileLayoutEnforcement.applyForcedLTR(to: stack) stack.setContentHuggingPriority(.defaultLow, for: .horizontal) wrap.setContentHuggingPriority(.defaultLow, for: .horizontal) wrap.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) NSLayoutConstraint.activate([ label.leftAnchor.constraint(equalTo: stack.leftAnchor), label.widthAnchor.constraint(equalTo: stack.widthAnchor), wrap.widthAnchor.constraint(equalTo: stack.widthAnchor) ]) return stack } private static func multilineLabeledStack(labelKey: String, field: NSTextField, placeholderKey: String, minHeight: CGFloat) -> NSView { let label = NSTextField(labelWithString: L(labelKey)) ProfileFormLocalization.tagLabel(label, key: labelKey) label.font = .systemFont(ofSize: 12, weight: .medium) label.textColor = ProfilePagePalette.secondaryText label.translatesAutoresizingMaskIntoConstraints = false ProfileLayoutEnforcement.applyLeftAlignedTextField(label) ProfileFormLocalization.tagPlaceholder(field, key: placeholderKey) field.translatesAutoresizingMaskIntoConstraints = false field.isEditable = true field.isSelectable = true field.isBordered = false field.drawsBackground = false field.focusRingType = .none field.font = .systemFont(ofSize: 14, weight: .regular) field.textColor = ProfilePagePalette.primaryText field.maximumNumberOfLines = 0 field.cell?.wraps = true field.cell?.isScrollable = false field.cell?.usesSingleLineMode = false ProfileFormLocalization.applyPlaceholder(L(placeholderKey), to: field) ProfileLayoutEnforcement.applyLeftAlignedTextField(field) let wrap = NSView() wrap.translatesAutoresizingMaskIntoConstraints = false wrap.wantsLayer = true wrap.layer?.backgroundColor = ProfilePagePalette.fieldFill.cgColor wrap.layer?.cornerRadius = 10 wrap.layer?.borderWidth = 1 wrap.layer?.borderColor = ProfilePagePalette.border.cgColor if #available(macOS 11.0, *) { wrap.layer?.cornerCurve = .continuous } wrap.addSubview(field) ProfileLayoutEnforcement.applyForcedLTR(to: wrap) NSLayoutConstraint.activate([ field.leftAnchor.constraint(equalTo: wrap.leftAnchor, constant: 12), field.rightAnchor.constraint(equalTo: wrap.rightAnchor, constant: -12), field.topAnchor.constraint(equalTo: wrap.topAnchor, constant: 10), field.bottomAnchor.constraint(equalTo: wrap.bottomAnchor, constant: -10), wrap.heightAnchor.constraint(greaterThanOrEqualToConstant: minHeight) ]) let stack = NSStackView(views: [label, wrap]) stack.orientation = .vertical stack.spacing = 8 stack.alignment = .leading stack.translatesAutoresizingMaskIntoConstraints = false stack.userInterfaceLayoutDirection = .leftToRight ProfileLayoutEnforcement.applyForcedLTR(to: stack) stack.setContentHuggingPriority(.defaultLow, for: .horizontal) wrap.setContentHuggingPriority(.defaultLow, for: .horizontal) NSLayoutConstraint.activate([ label.leftAnchor.constraint(equalTo: stack.leftAnchor), label.widthAnchor.constraint(equalTo: stack.widthAnchor), wrap.widthAnchor.constraint(equalTo: stack.widthAnchor) ]) return stack } private static func styleSingleLineField(_ field: NSTextField, placeholderKey: String) { ProfileFormLocalization.tagPlaceholder(field, key: placeholderKey) field.translatesAutoresizingMaskIntoConstraints = false field.isBordered = false field.drawsBackground = false field.focusRingType = .none field.font = .systemFont(ofSize: 14, weight: .regular) field.textColor = ProfilePagePalette.primaryText ProfileFormLocalization.applyPlaceholder(L(placeholderKey), to: field) field.cell?.usesSingleLineMode = true field.cell?.wraps = false field.cell?.isScrollable = true ProfileLayoutEnforcement.applyLeftAlignedTextField(field) } private static func roundedChrome(around field: NSTextField, minHeight: CGFloat) -> NSView { let wrap = NSView() wrap.translatesAutoresizingMaskIntoConstraints = false wrap.wantsLayer = true wrap.layer?.backgroundColor = ProfilePagePalette.fieldFill.cgColor wrap.layer?.cornerRadius = 10 wrap.layer?.borderWidth = 1 wrap.layer?.borderColor = ProfilePagePalette.border.cgColor if #available(macOS 11.0, *) { wrap.layer?.cornerCurve = .continuous } wrap.addSubview(field) ProfileLayoutEnforcement.applyForcedLTR(to: wrap) NSLayoutConstraint.activate([ field.leftAnchor.constraint(equalTo: wrap.leftAnchor, constant: 12), field.rightAnchor.constraint(equalTo: wrap.rightAnchor, constant: -12), field.centerYAnchor.constraint(equalTo: wrap.centerYAnchor), wrap.heightAnchor.constraint(greaterThanOrEqualToConstant: minHeight) ]) return wrap } } private final class EducationEntryView: NSView { var onDelete: (() -> Void)? private let subtitleLabel = NSTextField(labelWithString: String(format: L("Education %d"), 1)) private let deleteButton = NSButton() private let degreeField = NSTextField() private let institutionField = NSTextField() private let yearField = NSTextField() private var degreeInstitutionRow: ProfileDualFieldRow! override init(frame frameRect: NSRect) { super.init(frame: frameRect) configure() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func setEducationIndex(_ index: Int) { subtitleLabel.stringValue = String(format: L("Education %d"), index) } func setDeleteHidden(_ hidden: Bool) { deleteButton.isHidden = hidden } func applyCompactLayout(_ compact: Bool) { degreeInstitutionRow.setCompact(compact) } func capturePayload() -> EducationPayload { EducationPayload( degree: degreeField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines), institution: institutionField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines), year: yearField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) ) } func applyPayload(_ payload: EducationPayload) { degreeField.stringValue = payload.degree institutionField.stringValue = payload.institution yearField.stringValue = payload.year } private func configure() { userInterfaceLayoutDirection = .leftToRight ProfileLayoutEnforcement.applyForcedLTR(to: self) wantsLayer = true layer?.cornerRadius = 14 layer?.borderWidth = 1 layer?.borderColor = ProfilePagePalette.border.cgColor layer?.backgroundColor = ProfilePagePalette.cardBackground.cgColor layer?.masksToBounds = false layer?.shadowColor = NSColor.black.cgColor layer?.shadowOpacity = 0.04 layer?.shadowRadius = 10 layer?.shadowOffset = CGSize(width: 0, height: 5) if #available(macOS 11.0, *) { layer?.cornerCurve = .continuous } subtitleLabel.font = .systemFont(ofSize: 12, weight: .medium) subtitleLabel.textColor = ProfilePagePalette.secondaryText subtitleLabel.translatesAutoresizingMaskIntoConstraints = false ProfileLayoutEnforcement.applyLeftAlignedTextField(subtitleLabel) deleteButton.translatesAutoresizingMaskIntoConstraints = false deleteButton.isBordered = false deleteButton.bezelStyle = .regularSquare deleteButton.focusRingType = .none deleteButton.contentTintColor = ProfilePagePalette.destructive deleteButton.target = self deleteButton.action = #selector(didTapDelete) if #available(macOS 11.0, *) { deleteButton.image = NSImage(systemSymbolName: "trash", accessibilityDescription: L("Remove education")) deleteButton.imagePosition = .imageOnly ProfileFormLocalization.tagButton(deleteButton, key: "Remove education") } else { deleteButton.title = L("Remove") deleteButton.font = .systemFont(ofSize: 12, weight: .medium) ProfileFormLocalization.tagButton(deleteButton, key: "Remove") } let headerSpacer = NSView() headerSpacer.translatesAutoresizingMaskIntoConstraints = false headerSpacer.setContentHuggingPriority(.defaultLow, for: .horizontal) let headerRow = NSStackView(views: [subtitleLabel, headerSpacer, deleteButton]) headerRow.orientation = .horizontal headerRow.alignment = .centerY headerRow.distribution = .fill headerRow.spacing = 8 headerRow.userInterfaceLayoutDirection = .leftToRight ProfileLayoutEnforcement.applyForcedLTR(to: headerRow) headerRow.translatesAutoresizingMaskIntoConstraints = false let degreeGroup = WorkExperienceEntryView.labeledFieldStack( labelKey: "Degree / program *", field: degreeField, placeholderKey: "e.g., BSc Computer Science" ) let institutionGroup = WorkExperienceEntryView.labeledFieldStack( labelKey: "Institution *", field: institutionField, placeholderKey: "e.g., MIT" ) degreeInstitutionRow = ProfileDualFieldRow(left: degreeGroup, right: institutionGroup, spacing: 12) let yearGroup = WorkExperienceEntryView.labeledFieldStack( labelKey: "Year *", field: yearField, placeholderKey: "e.g., 2020" ) let inner = NSStackView(views: [headerRow, degreeInstitutionRow, yearGroup]) inner.orientation = .vertical inner.spacing = 14 inner.alignment = .leading inner.translatesAutoresizingMaskIntoConstraints = false inner.edgeInsets = NSEdgeInsets() inner.userInterfaceLayoutDirection = .leftToRight ProfileLayoutEnforcement.applyForcedLTR(to: inner) inner.pinAllArrangedSubviewWidthsEqualToStackWidth() addSubview(inner) NSLayoutConstraint.activate([ inner.leadingAnchor.constraint(equalTo: leadingAnchor, constant: ProfileEntryCardLayout.horizontalInset), inner.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -ProfileEntryCardLayout.horizontalInset), inner.topAnchor.constraint(equalTo: topAnchor, constant: 14), inner.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -14) ]) } override func layout() { super.layout() for field in [degreeField, institutionField, yearField] { if let wrap = field.superview, wrap.bounds.width > 2 { let w = max(1, wrap.bounds.width - 24) if abs(field.preferredMaxLayoutWidth - w) > 0.5 { field.preferredMaxLayoutWidth = w } } } guard let layer = layer, layer.shadowOpacity > 0 else { return } let r = layer.cornerRadius layer.shadowPath = CGPath(roundedRect: bounds, cornerWidth: r, cornerHeight: r, transform: nil) } @objc private func didTapDelete() { onDelete?() } func applyCurrentAppearance() { layer?.backgroundColor = ProfilePagePalette.cardBackground.cgColor layer?.borderColor = ProfilePagePalette.border.cgColor subtitleLabel.textColor = ProfilePagePalette.secondaryText deleteButton.contentTintColor = ProfilePagePalette.destructive ProfileThemeAppearance.refreshFormSubtree(self) } } // MARK: - Primary CTA private final class ProfilePrimaryButton: NSButton { private var trackingArea: NSTrackingArea? private var didPushCursor = false private var isHovering = false override init(frame frameRect: NSRect) { super.init(frame: frameRect) commonInit() } required init?(coder: NSCoder) { super.init(coder: coder) commonInit() } convenience init(title: String, target: AnyObject?, action: Selector?) { self.init(frame: .zero) self.title = title self.target = target self.action = action } private func commonInit() { bezelStyle = .rounded isBordered = false font = .systemFont(ofSize: 16, weight: .semibold) contentTintColor = ProfilePagePalette.ctaText wantsLayer = true layer?.cornerRadius = 14 if #available(macOS 11.0, *) { layer?.cornerCurve = .continuous } applyCurrentAppearance() } func applyCurrentAppearance() { contentTintColor = ProfilePagePalette.ctaText layer?.backgroundColor = (isHovering ? ProfilePagePalette.brandBlueHover : ProfilePagePalette.brandBlue).cgColor } override func updateTrackingAreas() { super.updateTrackingAreas() if let trackingArea { removeTrackingArea(trackingArea) } let area = NSTrackingArea( rect: bounds, options: [.activeInKeyWindow, .mouseEnteredAndExited, .inVisibleRect], owner: self, userInfo: nil ) addTrackingArea(area) trackingArea = area } override func mouseEntered(with event: NSEvent) { super.mouseEntered(with: event) isHovering = true applyCurrentAppearance() if !didPushCursor { NSCursor.pointingHand.push() didPushCursor = true } } override func mouseExited(with event: NSEvent) { super.mouseExited(with: event) isHovering = false applyCurrentAppearance() if didPushCursor { NSCursor.pop() didPushCursor = false } } override func viewWillMove(toWindow newWindow: NSWindow?) { super.viewWillMove(toWindow: newWindow) if newWindow == nil { isHovering = false if didPushCursor { NSCursor.pop() didPushCursor = false } } } }