// // 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 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 } } final class MyProfilePageView: NSView { /// Below this form content width, two-column rows stack vertically. private static let compactFormWidth: CGFloat = 640 private static let readableFormMaxWidth: CGFloat = 880 private static let horizontalPageInset: CGFloat = 24 private let scrollView = NSScrollView() private let documentView = NSView() private let cardView = NSView() private var readableFormWidthConstraint: NSLayoutConstraint! 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 let nameEmailRow = NSStackView() private let phoneJobRow = NSStackView() 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 updateConstraints() { updateReadableFormCardWidthConstraint() super.updateConstraints() } override func viewDidMoveToWindow() { super.viewDidMoveToWindow() needsUpdateConstraints = 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() } /// Keeps the card width at `min(readable max, clip − horizontal insets)` before constraint passes. If the constant is ever wider than the clip, Auto Layout can slide the card to the trailing edge and the form looks compressed / right‑aligned. private func updateReadableFormCardWidthConstraint() { let clipW = max(bounds.width, scrollView.bounds.width, scrollView.contentView.bounds.width) guard clipW > 1 else { return } let horizontalCardInset = Self.horizontalPageInset * 2 let maxUsable = clipW - horizontalCardInset let target = min(Self.readableFormMaxWidth, max(1, maxUsable)) if abs(readableFormWidthConstraint.constant - target) > 0.5 { readableFormWidthConstraint.constant = target } } /// 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 } // Start narrow so the first constraint pass cannot exceed a small host; `updateConstraints` sets the real width. readableFormWidthConstraint = cardView.widthAnchor.constraint(equalToConstant: 200) formStack.translatesAutoresizingMaskIntoConstraints = false formStack.orientation = .vertical formStack.alignment = .width formStack.distribution = .fill formStack.spacing = 24 formStack.edgeInsets = NSEdgeInsets(top: 32, left: 28, bottom: 32, right: 28) formStack.userInterfaceLayoutDirection = .leftToRight ProfileLayoutEnforcement.applyForcedLTR(to: formStack) formStack.setContentHuggingPriority(.defaultLow, for: .horizontal) formStack.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) addSubview(scrollView) scrollView.documentView = documentView documentView.addSubview(cardView) cardView.addSubview(formStack) NSLayoutConstraint.activate([ scrollView.leftAnchor.constraint(equalTo: leftAnchor), scrollView.rightAnchor.constraint(equalTo: rightAnchor), scrollView.topAnchor.constraint(equalTo: topAnchor), 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.widthAnchor.constraint(equalTo: scrollView.contentView.widthAnchor), documentView.topAnchor.constraint(equalTo: scrollView.contentView.topAnchor), documentView.bottomAnchor.constraint(equalTo: cardView.bottomAnchor, constant: Self.horizontalPageInset), cardView.leadingAnchor.constraint(equalTo: documentView.leadingAnchor, constant: Self.horizontalPageInset), cardView.trailingAnchor.constraint(lessThanOrEqualTo: documentView.trailingAnchor, constant: -Self.horizontalPageInset), cardView.topAnchor.constraint(equalTo: documentView.topAnchor, constant: Self.horizontalPageInset), readableFormWidthConstraint, formStack.leadingAnchor.constraint(equalTo: cardView.leadingAnchor), formStack.trailingAnchor.constraint(equalTo: cardView.trailingAnchor), formStack.widthAnchor.constraint(equalTo: cardView.widthAnchor), formStack.topAnchor.constraint(equalTo: cardView.topAnchor), formStack.bottomAnchor.constraint(equalTo: cardView.bottomAnchor) ]) formStack.addArrangedSubview(labeledGroup(title: "Profile Name *", field: profileNameField, placeholder: "Marketing Director Profile")) formStack.addArrangedSubview(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") configureTwoColumnRow(nameEmailRow, left: nameGroup, right: emailGroup) pinEqualColumnWidths(in: nameEmailRow) formStack.addArrangedSubview(nameEmailRow) let phoneGroup = labeledGroup(title: "Phone", field: phoneField, placeholder: "+1 (555) 123-4567") let jobGroup = labeledGroup(title: "Job Title *", field: jobTitleField, placeholder: "Software Engineer") configureTwoColumnRow(phoneJobRow, left: phoneGroup, right: jobGroup) pinEqualColumnWidths(in: phoneJobRow) formStack.addArrangedSubview(phoneJobRow) ProfileLayoutEnforcement.applyForcedLTR(to: nameEmailRow) ProfileLayoutEnforcement.applyForcedLTR(to: phoneJobRow) formStack.addArrangedSubview(labeledGroup(title: "Address", field: addressField, placeholder: "123 Main St, City, State, ZIP")) formStack.addArrangedSubview(careerSummaryBlock()) formStack.addArrangedSubview(horizontalSeparator()) formStack.addArrangedSubview(workExperienceSection()) formStack.addArrangedSubview(horizontalSeparator()) formStack.addArrangedSubview(educationSection()) formStack.addArrangedSubview(horizontalSeparator()) formStack.addArrangedSubview( multilineProfileBlock( title: "Certificates / Rewards", placeholder: "List your certificates and awards...", field: certificatesField, minHeight: 100 ) ) formStack.addArrangedSubview(horizontalSeparator()) formStack.addArrangedSubview( multilineProfileBlock( title: "Interests", placeholder: "List your interests and hobbies...", field: interestsField, minHeight: 100 ) ) formStack.addArrangedSubview(horizontalSeparator()) formStack.addArrangedSubview( multilineProfileBlock( title: "Languages", placeholder: "List languages you speak (e.g., English - Native, Spanish - Fluent)...", field: languagesField, minHeight: 100 ) ) formStack.addArrangedSubview(horizontalSeparator()) formStack.addArrangedSubview(referralBlock()) formStack.addArrangedSubview(saveButtonHost()) saveButton.target = self saveButton.action = #selector(didTapSave) appendWorkExperienceEntry() appendEducationEntry() } private func applyResponsiveRowsIfNeeded() { let w = cardView.bounds.width guard w > 1 else { return } let formWidth = max(0, w - formStack.edgeInsets.left - formStack.edgeInsets.right) let compact = formWidth < Self.compactFormWidth guard compact != lastCompactLayout else { return } lastCompactLayout = compact let orientation: NSUserInterfaceLayoutOrientation = compact ? .vertical : .horizontal let rowSpacing: CGFloat = compact ? 16 : 12 nameEmailRow.orientation = orientation nameEmailRow.spacing = rowSpacing phoneJobRow.orientation = orientation phoneJobRow.spacing = rowSpacing nameEmailRow.distribution = compact ? .fill : .fillEqually phoneJobRow.distribution = compact ? .fill : .fillEqually for entry in workExperienceEntries { entry.applyCompactLayout(compact) } for entry in educationEntries { entry.applyCompactLayout(compact) } } /// Keeps two columns the same width when the row is horizontal; avoids NSTextField intrinsic width fighting the stack. private func pinEqualColumnWidths(in row: NSStackView) { guard row.arrangedSubviews.count == 2 else { return } let left = row.arrangedSubviews[0] let right = row.arrangedSubviews[1] left.widthAnchor.constraint(equalTo: right.widthAnchor).isActive = true } private func configureTwoColumnRow(_ row: NSStackView, left: NSView, right: NSView) { row.translatesAutoresizingMaskIntoConstraints = false row.orientation = .horizontal row.spacing = 12 row.distribution = .fillEqually row.alignment = .top row.userInterfaceLayoutDirection = .leftToRight ProfileLayoutEnforcement.applyForcedLTR(to: row) row.setContentHuggingPriority(.defaultLow, for: .horizontal) row.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) row.addArrangedSubview(left) row.addArrangedSubview(right) } 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 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(.defaultHigh, 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 // `.width` stretches label + field chrome to the row width; `.leading` collapses to intrinsic width and caused right-edge compression. stack.alignment = .width 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([ 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 = .width stack.translatesAutoresizingMaskIntoConstraints = false stack.userInterfaceLayoutDirection = .leftToRight ProfileLayoutEnforcement.applyForcedLTR(to: stack) stack.setContentHuggingPriority(.defaultLow, for: .horizontal) wrap.setContentHuggingPriority(.defaultLow, for: .horizontal) 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 = .width stack.translatesAutoresizingMaskIntoConstraints = false stack.userInterfaceLayoutDirection = .leftToRight ProfileLayoutEnforcement.applyForcedLTR(to: stack) stack.setContentHuggingPriority(.defaultLow, for: .horizontal) wrap.setContentHuggingPriority(.defaultLow, for: .horizontal) 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 = .width 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) } 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 = .width workExperienceRowsStack.userInterfaceLayoutDirection = .leftToRight ProfileLayoutEnforcement.applyForcedLTR(to: workExperienceRowsStack) let outer = NSStackView(views: [headerRow, workExperienceRowsStack]) outer.orientation = .vertical outer.spacing = 16 outer.alignment = .width outer.translatesAutoresizingMaskIntoConstraints = false outer.userInterfaceLayoutDirection = .leftToRight ProfileLayoutEnforcement.applyForcedLTR(to: outer) 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 = .width educationRowsStack.userInterfaceLayoutDirection = .leftToRight ProfileLayoutEnforcement.applyForcedLTR(to: educationRowsStack) let outer = NSStackView(views: [headerRow, educationRowsStack]) outer.orientation = .vertical outer.spacing = 16 outer.alignment = .width outer.translatesAutoresizingMaskIntoConstraints = false outer.userInterfaceLayoutDirection = .leftToRight ProfileLayoutEnforcement.applyForcedLTR(to: outer) 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) 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) 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 let host = NSView() host.translatesAutoresizingMaskIntoConstraints = false host.userInterfaceLayoutDirection = .leftToRight ProfileLayoutEnforcement.applyForcedLTR(to: host) host.addSubview(saveButton) NSLayoutConstraint.activate([ saveButton.leadingAnchor.constraint(equalTo: host.leadingAnchor), saveButton.topAnchor.constraint(equalTo: host.topAnchor), saveButton.bottomAnchor.constraint(equalTo: host.bottomAnchor), saveButton.heightAnchor.constraint(equalToConstant: 48), saveButton.trailingAnchor.constraint(lessThanOrEqualTo: host.trailingAnchor) ]) return host } @objc private func didTapSave() { // UI shell only; wire persistence when profiles are stored. } } // MARK: - Work experience & education rows 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 let jobCompanyRow = NSStackView() 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.orientation = compact ? .vertical : .horizontal jobCompanyRow.spacing = compact ? 12 : 12 jobCompanyRow.distribution = compact ? .fill : .fillEqually } 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") configureTwoColumnRow(jobCompanyRow, left: jobGroup, right: companyGroup) pinEqualColumnWidths(in: jobCompanyRow) 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 = .width inner.translatesAutoresizingMaskIntoConstraints = false inner.edgeInsets = NSEdgeInsets(top: 16, left: 18, bottom: 16, right: 18) inner.userInterfaceLayoutDirection = .leftToRight ProfileLayoutEnforcement.applyForcedLTR(to: inner) addSubview(inner) NSLayoutConstraint.activate([ inner.leftAnchor.constraint(equalTo: leftAnchor), inner.rightAnchor.constraint(equalTo: rightAnchor), inner.topAnchor.constraint(equalTo: topAnchor), inner.bottomAnchor.constraint(equalTo: bottomAnchor) ]) } private func configureTwoColumnRow(_ row: NSStackView, left: NSView, right: NSView) { row.translatesAutoresizingMaskIntoConstraints = false row.orientation = .horizontal row.spacing = 12 row.distribution = .fillEqually row.alignment = .top row.userInterfaceLayoutDirection = .leftToRight ProfileLayoutEnforcement.applyForcedLTR(to: row) row.setContentHuggingPriority(.defaultLow, for: .horizontal) row.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) row.addArrangedSubview(left) row.addArrangedSubview(right) } 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) } private func pinEqualColumnWidths(in row: NSStackView) { guard row.arrangedSubviews.count == 2 else { return } let left = row.arrangedSubviews[0] let right = row.arrangedSubviews[1] left.widthAnchor.constraint(equalTo: right.widthAnchor).isActive = true } @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 = .width stack.translatesAutoresizingMaskIntoConstraints = false stack.userInterfaceLayoutDirection = .leftToRight ProfileLayoutEnforcement.applyForcedLTR(to: stack) NSLayoutConstraint.activate([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 = .width stack.translatesAutoresizingMaskIntoConstraints = false stack.userInterfaceLayoutDirection = .leftToRight ProfileLayoutEnforcement.applyForcedLTR(to: stack) 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 let degreeInstitutionRow = NSStackView() 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.orientation = compact ? .vertical : .horizontal degreeInstitutionRow.spacing = 12 degreeInstitutionRow.distribution = compact ? .fill : .fillEqually } 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" ) configureTwoColumnRow(degreeInstitutionRow, left: degreeGroup, right: institutionGroup) pinEqualColumnWidths(in: degreeInstitutionRow) 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 = .width inner.translatesAutoresizingMaskIntoConstraints = false inner.edgeInsets = NSEdgeInsets(top: 14, left: 18, bottom: 14, right: 18) inner.userInterfaceLayoutDirection = .leftToRight ProfileLayoutEnforcement.applyForcedLTR(to: inner) addSubview(inner) NSLayoutConstraint.activate([ inner.leftAnchor.constraint(equalTo: leftAnchor), inner.rightAnchor.constraint(equalTo: rightAnchor), inner.topAnchor.constraint(equalTo: topAnchor), inner.bottomAnchor.constraint(equalTo: bottomAnchor) ]) } private func configureTwoColumnRow(_ row: NSStackView, left: NSView, right: NSView) { row.translatesAutoresizingMaskIntoConstraints = false row.orientation = .horizontal row.spacing = 12 row.distribution = .fillEqually row.alignment = .top row.userInterfaceLayoutDirection = .leftToRight ProfileLayoutEnforcement.applyForcedLTR(to: row) row.setContentHuggingPriority(.defaultLow, for: .horizontal) row.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) row.addArrangedSubview(left) row.addArrangedSubview(right) } override func layout() { super.layout() guard let layer = layer, layer.shadowOpacity > 0 else { return } let r = layer.cornerRadius layer.shadowPath = CGPath(roundedRect: bounds, cornerWidth: r, cornerHeight: r, transform: nil) } private func pinEqualColumnWidths(in row: NSStackView) { guard row.arrangedSubviews.count == 2 else { return } let left = row.arrangedSubviews[0] let right = row.arrangedSubviews[1] left.widthAnchor.constraint(equalTo: right.widthAnchor).isActive = true } @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: 15, weight: .semibold) contentTintColor = .white wantsLayer = true layer?.cornerRadius = 12 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 } } }