// // MyProfilePageView.swift // App for Indeed // // Light-theme profile editor: card layout, adaptive two-column rows, and // vertical scrolling when the window is short. // import Cocoa private enum ProfilePagePalette { static let brandBlue = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 1) static let brandBlueHover = NSColor(srgbRed: 28 / 255, green: 70 / 255, blue: 140 / 255, alpha: 1) static let pageBackground = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1) static let cardBackground = NSColor(srgbRed: 252 / 255, green: 252 / 255, blue: 252 / 255, alpha: 1) static let fieldFill = NSColor(srgbRed: 247 / 255, green: 247 / 255, blue: 247 / 255, alpha: 1) static let primaryText = NSColor(srgbRed: 45 / 255, green: 45 / 255, blue: 45 / 255, alpha: 1) static let secondaryText = NSColor(srgbRed: 118 / 255, green: 118 / 255, blue: 118 / 255, alpha: 1) static let border = NSColor(srgbRed: 212 / 255, green: 210 / 255, blue: 208 / 255, alpha: 1) static let destructive = NSColor(srgbRed: 220 / 255, green: 38 / 255, blue: 38 / 255, alpha: 1) } /// 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 } } } /// 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: "Save Profile →", target: nil, action: nil) private var nameEmailRow: ProfileDualFieldRow! private var phoneJobRow: ProfileDualFieldRow! private let topChrome = NSView() private let backButton = NSButton(title: "← 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? /// 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() } required init?(coder: NSCoder) { super.init(coder: coder) setup() } 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 = "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(title: "Profile Name *", field: profileNameField, placeholder: "Marketing Director Profile") ) addFullWidthArrangedSubview(sectionHeading("Personal Information")) let nameGroup = labeledGroup(title: "Full Name *", field: fullNameField, placeholder: "John Doe") let emailGroup = labeledGroup(title: "Email *", field: emailField, placeholder: "john@example.com") nameEmailRow = ProfileDualFieldRow(left: nameGroup, right: emailGroup, spacing: 12) addFullWidthArrangedSubview(nameEmailRow) let phoneGroup = labeledGroup(title: "Phone", field: phoneField, placeholder: "+1 (555) 123-4567") let jobGroup = labeledGroup(title: "Job Title *", field: jobTitleField, placeholder: "Software Engineer") phoneJobRow = ProfileDualFieldRow(left: phoneGroup, right: jobGroup, spacing: 12) addFullWidthArrangedSubview(phoneJobRow) addFullWidthArrangedSubview( labeledGroup(title: "Address", field: addressField, placeholder: "123 Main St, City, State, ZIP") ) addFullWidthArrangedSubview(careerSummaryBlock()) addFullWidthArrangedSubview(horizontalSeparator()) addFullWidthArrangedSubview(workExperienceSection()) addFullWidthArrangedSubview(horizontalSeparator()) addFullWidthArrangedSubview(educationSection()) addFullWidthArrangedSubview(horizontalSeparator()) addFullWidthArrangedSubview( multilineProfileBlock( title: "Certificates / Rewards", placeholder: "List your certificates and awards...", field: certificatesField, minHeight: 100 ) ) addFullWidthArrangedSubview(horizontalSeparator()) addFullWidthArrangedSubview( multilineProfileBlock( title: "Interests", placeholder: "List your interests and hobbies...", field: interestsField, minHeight: 100 ) ) addFullWidthArrangedSubview(horizontalSeparator()) addFullWidthArrangedSubview( multilineProfileBlock( title: "Languages", placeholder: "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 contextLabel.stringValue = "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 contextLabel.stringValue = "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(_ text: String) -> NSView { let label = NSTextField(labelWithString: text) 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(title: String, field: NSTextField, placeholder: String) -> NSView { let label = NSTextField(labelWithString: title) label.font = .systemFont(ofSize: 12, weight: .medium) label.textColor = ProfilePagePalette.secondaryText label.translatesAutoresizingMaskIntoConstraints = false label.setContentHuggingPriority(.defaultLow, for: .horizontal) ProfileLayoutEnforcement.applyLeftAlignedTextField(label) styleSingleLineField(field, placeholder: placeholder) 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, placeholder: String) { 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) let paragraph = ProfileLayoutEnforcement.leftAlignedParagraphStyle() field.placeholderAttributedString = NSAttributedString( string: placeholder, attributes: [ .foregroundColor: ProfilePagePalette.secondaryText, .font: NSFont.systemFont(ofSize: 14, weight: .regular), .paragraphStyle: paragraph ] ) 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: "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) careerField.placeholderAttributedString = NSAttributedString( string: "Brief overview of your professional background and key achievements...", attributes: [ .foregroundColor: ProfilePagePalette.secondaryText, .font: NSFont.systemFont(ofSize: 14, weight: .regular), .paragraphStyle: ProfileLayoutEnforcement.leftAlignedParagraphStyle() ] ) 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(title: String, placeholder: String, field: NSTextField, minHeight: CGFloat) -> NSView { let label = NSTextField(labelWithString: title) label.font = .systemFont(ofSize: 12, weight: .medium) label.textColor = ProfilePagePalette.secondaryText label.translatesAutoresizingMaskIntoConstraints = false ProfileLayoutEnforcement.applyLeftAlignedTextField(label) 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) field.placeholderAttributedString = NSAttributedString( string: placeholder, attributes: [ .foregroundColor: ProfilePagePalette.secondaryText, .font: NSFont.systemFont(ofSize: 14, weight: .regular), .paragraphStyle: ProfileLayoutEnforcement.leftAlignedParagraphStyle() ] ) 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: "Referral (Optional)") label.font = .systemFont(ofSize: 12, weight: .medium) label.textColor = ProfilePagePalette.secondaryText label.translatesAutoresizingMaskIntoConstraints = false ProfileLayoutEnforcement.applyLeftAlignedTextField(label) styleSingleLineField(referralField, placeholder: "Referred by (Company/Person Name)") let wrap = roundedFieldChrome(containing: referralField, minHeight: 40) let helper = NSTextField(wrappingLabelWithString: "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: "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: "+ Add Another", target: self, action: #selector(didTapAddWorkExperience)) 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: "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: "+ Add Another", target: self, action: #selector(didTapAddEducation)) 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("Profile name") } if profile.personal.fullName.isEmpty { missing.append("Full Name") } if profile.personal.email.isEmpty { missing.append("Email") } if profile.personal.jobTitle.isEmpty { missing.append("Job Title") } guard missing.isEmpty else { let alert = NSAlert() alert.messageText = "Complete required fields" alert.informativeText = "Please fill in: " + missing.joined(separator: ", ") + "." alert.alertStyle = .informational alert.addButton(withTitle: "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: "Experience 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 = "Experience \(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: "Remove experience") deleteButton.imagePosition = .imageOnly } else { deleteButton.title = "Remove" deleteButton.font = .systemFont(ofSize: 12, weight: .medium) } 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(title: "Job Title *", field: jobTitleField, placeholder: "e.g., Software Engineer") let companyGroup = Self.labeledFieldStack(title: "Company Name *", field: companyField, placeholder: "e.g., Google") jobCompanyRow = ProfileDualFieldRow(left: jobGroup, right: companyGroup, spacing: 12) let durationGroup = Self.labeledFieldStack(title: "Duration *", field: durationField, placeholder: "e.g., Jan 2020 - Present") let descriptionGroup = Self.multilineLabeledStack( title: "Description", field: descriptionField, placeholder: "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?() } fileprivate static func labeledFieldStack(title: String, field: NSTextField, placeholder: String) -> NSView { let label = NSTextField(labelWithString: title) label.font = .systemFont(ofSize: 12, weight: .medium) label.textColor = ProfilePagePalette.secondaryText label.translatesAutoresizingMaskIntoConstraints = false ProfileLayoutEnforcement.applyLeftAlignedTextField(label) styleSingleLineField(field, placeholder: placeholder) 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(title: String, field: NSTextField, placeholder: String, minHeight: CGFloat) -> NSView { let label = NSTextField(labelWithString: title) label.font = .systemFont(ofSize: 12, weight: .medium) label.textColor = ProfilePagePalette.secondaryText label.translatesAutoresizingMaskIntoConstraints = false ProfileLayoutEnforcement.applyLeftAlignedTextField(label) 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.placeholderAttributedString = NSAttributedString( string: placeholder, attributes: [ .foregroundColor: ProfilePagePalette.secondaryText, .font: NSFont.systemFont(ofSize: 14, weight: .regular), .paragraphStyle: ProfileLayoutEnforcement.leftAlignedParagraphStyle() ] ) 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, placeholder: String) { field.translatesAutoresizingMaskIntoConstraints = false field.isBordered = false field.drawsBackground = false field.focusRingType = .none field.font = .systemFont(ofSize: 14, weight: .regular) field.textColor = ProfilePagePalette.primaryText field.placeholderAttributedString = NSAttributedString( string: placeholder, attributes: [ .foregroundColor: ProfilePagePalette.secondaryText, .font: NSFont.systemFont(ofSize: 14, weight: .regular), .paragraphStyle: ProfileLayoutEnforcement.leftAlignedParagraphStyle() ] ) 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: "Education 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 = "Education \(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: "Remove education") deleteButton.imagePosition = .imageOnly } else { deleteButton.title = "Remove" deleteButton.font = .systemFont(ofSize: 12, weight: .medium) } 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( title: "Degree / program *", field: degreeField, placeholder: "e.g., BSc Computer Science" ) let institutionGroup = WorkExperienceEntryView.labeledFieldStack( title: "Institution *", field: institutionField, placeholder: "e.g., MIT" ) degreeInstitutionRow = ProfileDualFieldRow(left: degreeGroup, right: institutionGroup, spacing: 12) let yearGroup = WorkExperienceEntryView.labeledFieldStack( title: "Year *", field: yearField, placeholder: "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?() } } // MARK: - Primary CTA private final class ProfilePrimaryButton: NSButton { private var trackingArea: NSTrackingArea? private var didPushCursor = 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 = .white wantsLayer = true layer?.cornerRadius = 14 if #available(macOS 11.0, *) { layer?.cornerCurve = .continuous } layer?.backgroundColor = 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) layer?.backgroundColor = ProfilePagePalette.brandBlueHover.cgColor if !didPushCursor { NSCursor.pointingHand.push() didPushCursor = true } } override func mouseExited(with event: NSEvent) { super.mouseExited(with: event) layer?.backgroundColor = ProfilePagePalette.brandBlue.cgColor if didPushCursor { NSCursor.pop() didPushCursor = false } } override func viewWillMove(toWindow newWindow: NSWindow?) { super.viewWillMove(toWindow: newWindow) if newWindow == nil, didPushCursor { NSCursor.pop() didPushCursor = false } } }