// // 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) } final class MyProfilePageView: NSView { private static let compactFormWidth: CGFloat = 520 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 saveButton = ProfilePrimaryButton(title: "Save Profile →", target: nil, action: nil) private let nameEmailRow = NSStackView() private let phoneJobRow = NSStackView() private var lastCompactLayout: Bool? /// 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 layout() { super.layout() applyResponsiveRowsIfNeeded() } private func setup() { wantsLayer = true layer?.backgroundColor = ProfilePagePalette.pageBackground.cgColor userInterfaceLayoutDirection = .leftToRight scrollView.translatesAutoresizingMaskIntoConstraints = false scrollView.userInterfaceLayoutDirection = .leftToRight 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 documentView.translatesAutoresizingMaskIntoConstraints = false documentView.userInterfaceLayoutDirection = .leftToRight cardView.translatesAutoresizingMaskIntoConstraints = false cardView.wantsLayer = true cardView.layer?.backgroundColor = ProfilePagePalette.cardBackground.cgColor cardView.layer?.cornerRadius = 16 cardView.layer?.borderWidth = 1 cardView.layer?.borderColor = ProfilePagePalette.border.cgColor cardView.userInterfaceLayoutDirection = .leftToRight if #available(macOS 11.0, *) { cardView.layer?.cornerCurve = .continuous } formStack.translatesAutoresizingMaskIntoConstraints = false formStack.orientation = .vertical formStack.alignment = .width formStack.distribution = .fill formStack.spacing = 20 formStack.edgeInsets = NSEdgeInsets(top: 28, left: 22, bottom: 28, right: 22) formStack.userInterfaceLayoutDirection = .leftToRight formStack.setContentHuggingPriority(.defaultLow, for: .horizontal) formStack.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) addSubview(scrollView) scrollView.documentView = documentView documentView.addSubview(cardView) cardView.addSubview(formStack) NSLayoutConstraint.activate([ scrollView.leadingAnchor.constraint(equalTo: leadingAnchor), scrollView.trailingAnchor.constraint(equalTo: trailingAnchor), scrollView.topAnchor.constraint(equalTo: topAnchor), scrollView.bottomAnchor.constraint(equalTo: bottomAnchor), // Pin left and right to the clip view’s geometric edges so the document spans the full visible // width (leading/trailing + width alone can leave a narrow strip on the wrong side in edge cases). documentView.leftAnchor.constraint(equalTo: scrollView.contentView.leftAnchor), documentView.rightAnchor.constraint(equalTo: scrollView.contentView.rightAnchor), documentView.topAnchor.constraint(equalTo: scrollView.contentView.topAnchor), cardView.leadingAnchor.constraint(equalTo: documentView.leadingAnchor, constant: 20), cardView.trailingAnchor.constraint(equalTo: documentView.trailingAnchor, constant: -20), cardView.topAnchor.constraint(equalTo: documentView.topAnchor, constant: 16), cardView.bottomAnchor.constraint(equalTo: documentView.bottomAnchor, constant: -24), formStack.leadingAnchor.constraint(equalTo: cardView.leadingAnchor), formStack.trailingAnchor.constraint(equalTo: cardView.trailingAnchor), 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) formStack.addArrangedSubview(labeledGroup(title: "Address", field: addressField, placeholder: "123 Main St, City, State, ZIP")) formStack.addArrangedSubview(careerSummaryBlock()) formStack.addArrangedSubview(saveButtonHost()) saveButton.target = self saveButton.action = #selector(didTapSave) } 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 } /// 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 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.alignment = .left label.baseWritingDirection = .leftToRight label.translatesAutoresizingMaskIntoConstraints = false label.setContentHuggingPriority(.defaultHigh, for: .horizontal) let spacer = NSView() spacer.translatesAutoresizingMaskIntoConstraints = false spacer.setContentHuggingPriority(.defaultLow, for: .horizontal) let row = NSStackView(views: [label, spacer]) row.orientation = .horizontal row.alignment = .centerY row.spacing = 0 row.userInterfaceLayoutDirection = .leftToRight 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.alignment = .left label.baseWritingDirection = .leftToRight label.translatesAutoresizingMaskIntoConstraints = false label.setContentHuggingPriority(.defaultHigh, for: .horizontal) 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 the title at the left edge; width match keeps the field full width. stack.alignment = .leading stack.translatesAutoresizingMaskIntoConstraints = false stack.userInterfaceLayoutDirection = .leftToRight 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) field.placeholderAttributedString = NSAttributedString( string: placeholder, attributes: [ .foregroundColor: ProfilePagePalette.secondaryText, .font: NSFont.systemFont(ofSize: 14, weight: .regular) ] ) field.cell?.usesSingleLineMode = true field.cell?.wraps = false field.cell?.isScrollable = true field.baseWritingDirection = .leftToRight field.alignment = .left } 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) 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.alignment = .left label.translatesAutoresizingMaskIntoConstraints = false 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.baseWritingDirection = .leftToRight careerField.alignment = .left careerField.placeholderAttributedString = NSAttributedString( string: "Brief overview of your professional background and key achievements...", attributes: [ .foregroundColor: ProfilePagePalette.secondaryText, .font: NSFont.systemFont(ofSize: 14, weight: .regular) ] ) 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) 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: 120) ]) let stack = NSStackView(views: [label, wrap]) stack.orientation = .vertical stack.spacing = 8 stack.alignment = .width stack.translatesAutoresizingMaskIntoConstraints = false stack.userInterfaceLayoutDirection = .leftToRight stack.setContentHuggingPriority(.defaultLow, for: .horizontal) wrap.setContentHuggingPriority(.defaultLow, for: .horizontal) return stack } private func saveButtonHost() -> NSView { saveButton.translatesAutoresizingMaskIntoConstraints = false let host = NSView() host.translatesAutoresizingMaskIntoConstraints = false host.userInterfaceLayoutDirection = .leftToRight 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: - 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 } } }