// // 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 import UniformTypeIdentifiers 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 avatarWell = NSColor(srgbRed: 232 / 255, green: 232 / 255, blue: 232 / 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 avatarImageView = NSImageView() private let uploadPhotoButton = NSButton(title: "Upload Photo", target: nil, action: nil) private let saveButton = ProfilePrimaryButton(title: "Save Profile →", target: nil, action: nil) private let nameEmailRow = NSStackView() private let phoneJobRow = NSStackView() private var lastCompactLayout: Bool? 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 scrollView.translatesAutoresizingMaskIntoConstraints = false scrollView.hasVerticalScroller = true scrollView.hasHorizontalScroller = false scrollView.autohidesScrollers = true scrollView.drawsBackground = false scrollView.borderType = .noBorder scrollView.scrollerStyle = .overlay scrollView.automaticallyAdjustsContentInsets = false 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.spacing = 20 formStack.edgeInsets = NSEdgeInsets(top: 28, left: 28, bottom: 28, right: 28) formStack.userInterfaceLayoutDirection = .leftToRight 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), documentView.leadingAnchor.constraint(equalTo: scrollView.contentView.leadingAnchor), documentView.trailingAnchor.constraint(equalTo: scrollView.contentView.trailingAnchor), documentView.topAnchor.constraint(equalTo: scrollView.contentView.topAnchor), documentView.widthAnchor.constraint(equalTo: scrollView.contentView.widthAnchor), cardView.leadingAnchor.constraint(equalTo: documentView.leadingAnchor, constant: 24), cardView.trailingAnchor.constraint(equalTo: documentView.trailingAnchor, constant: -24), 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(profileImageBlock()) formStack.addArrangedSubview(saveButtonHost()) uploadPhotoButton.target = self uploadPhotoButton.action = #selector(didTapUploadPhoto) 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) -> NSTextField { let label = NSTextField(labelWithString: text) label.font = .systemFont(ofSize: 15, weight: .semibold) label.textColor = ProfilePagePalette.primaryText label.alignment = .left label.translatesAutoresizingMaskIntoConstraints = false label.setContentHuggingPriority(.defaultLow, for: .horizontal) return label } 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.translatesAutoresizingMaskIntoConstraints = false styleSingleLineField(field, placeholder: placeholder) let wrap = roundedFieldChrome(containing: field, minHeight: 40) 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) wrap.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) 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 } 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.leadingAnchor.constraint(equalTo: wrap.leadingAnchor, constant: 12), field.trailingAnchor.constraint(equalTo: wrap.trailingAnchor, 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.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.leadingAnchor.constraint(equalTo: wrap.leadingAnchor, constant: 12), careerField.trailingAnchor.constraint(equalTo: wrap.trailingAnchor, 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 profileImageBlock() -> NSView { let title = NSTextField(labelWithString: "Profile Image (Optional)") title.font = .systemFont(ofSize: 12, weight: .medium) title.textColor = ProfilePagePalette.secondaryText title.translatesAutoresizingMaskIntoConstraints = false let avatarHost = NSView() avatarHost.translatesAutoresizingMaskIntoConstraints = false avatarHost.wantsLayer = true avatarHost.layer?.backgroundColor = ProfilePagePalette.avatarWell.cgColor avatarHost.layer?.cornerRadius = 36 avatarHost.layer?.masksToBounds = true avatarImageView.translatesAutoresizingMaskIntoConstraints = false avatarImageView.imageScaling = .scaleProportionallyUpOrDown avatarImageView.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 36, weight: .light) avatarImageView.image = NSImage(systemSymbolName: "person.crop.circle.fill", accessibilityDescription: nil) avatarImageView.contentTintColor = ProfilePagePalette.secondaryText avatarHost.addSubview(avatarImageView) NSLayoutConstraint.activate([ avatarHost.widthAnchor.constraint(equalToConstant: 72), avatarHost.heightAnchor.constraint(equalToConstant: 72), avatarImageView.centerXAnchor.constraint(equalTo: avatarHost.centerXAnchor), avatarImageView.centerYAnchor.constraint(equalTo: avatarHost.centerYAnchor), avatarImageView.widthAnchor.constraint(equalToConstant: 44), avatarImageView.heightAnchor.constraint(equalToConstant: 44) ]) uploadPhotoButton.translatesAutoresizingMaskIntoConstraints = false uploadPhotoButton.bezelStyle = .rounded uploadPhotoButton.controlSize = .large uploadPhotoButton.font = .systemFont(ofSize: 13, weight: .medium) if let image = NSImage(systemSymbolName: "arrow.up.circle", accessibilityDescription: nil) { uploadPhotoButton.image = image uploadPhotoButton.imagePosition = .imageLeading uploadPhotoButton.contentTintColor = ProfilePagePalette.brandBlue } let hint = NSTextField(wrappingLabelWithString: "Recommended: Square image, max 2MB") hint.font = .systemFont(ofSize: 11, weight: .regular) hint.textColor = ProfilePagePalette.secondaryText hint.maximumNumberOfLines = 0 let rightColumn = NSStackView(views: [uploadPhotoButton, hint]) rightColumn.orientation = .vertical rightColumn.alignment = .leading rightColumn.spacing = 6 rightColumn.translatesAutoresizingMaskIntoConstraints = false let row = NSStackView(views: [avatarHost, rightColumn]) row.orientation = .horizontal row.alignment = .centerY row.spacing = 16 row.translatesAutoresizingMaskIntoConstraints = false let stack = NSStackView(views: [title, row]) stack.orientation = .vertical stack.spacing = 12 stack.alignment = .width stack.translatesAutoresizingMaskIntoConstraints = false stack.userInterfaceLayoutDirection = .leftToRight row.setContentHuggingPriority(.defaultLow, for: .horizontal) row.alignment = .leading 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.trailingAnchor.constraint(equalTo: host.trailingAnchor), saveButton.topAnchor.constraint(equalTo: host.topAnchor), saveButton.bottomAnchor.constraint(equalTo: host.bottomAnchor), saveButton.heightAnchor.constraint(equalToConstant: 48) ]) return host } @objc private func didTapUploadPhoto() { let panel = NSOpenPanel() panel.allowedContentTypes = [UTType.image] panel.allowsMultipleSelection = false panel.canChooseDirectories = false guard let window else { return } panel.beginSheetModal(for: window) { [weak self] response in guard response == .OK, let url = panel.url else { return } if let image = NSImage(contentsOf: url) { self?.avatarImageView.image = image self?.avatarImageView.contentTintColor = nil self?.avatarImageView.imageScaling = .scaleAxesIndependently } } } @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 } } }