Procházet zdrojové kódy

Fix profile form layout: pin card edges instead of fixed width

The profile card used an explicit width plus a trailing inequality, which could conflict with the scroll clip and shift content to the right. Pin the card leading and trailing to the document view so the form always spans the available width.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 před 2 měsíci
rodič
revize
3d968e7bfa
1 změnil soubory, kde provedl 143 přidání a 145 odebrání
  1. 143 145
      App for Indeed/Views/MyProfilePageView.swift

+ 143 - 145
App for Indeed/Views/MyProfilePageView.swift

@@ -44,16 +44,77 @@ private enum ProfileLayoutEnforcement {
     }
     }
 }
 }
 
 
+/// 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 {
 final class MyProfilePageView: NSView {
     /// Below this form content width, two-column rows stack vertically.
     /// Below this form content width, two-column rows stack vertically.
     private static let compactFormWidth: CGFloat = 640
     private static let compactFormWidth: CGFloat = 640
-    private static let readableFormMaxWidth: CGFloat = 880
     private static let horizontalPageInset: CGFloat = 24
     private static let horizontalPageInset: CGFloat = 24
 
 
     private let scrollView = NSScrollView()
     private let scrollView = NSScrollView()
     private let documentView = NSView()
     private let documentView = NSView()
     private let cardView = NSView()
     private let cardView = NSView()
-    private var readableFormWidthConstraint: NSLayoutConstraint!
     private let formStack = NSStackView()
     private let formStack = NSStackView()
 
 
     private let profileNameField = NSTextField()
     private let profileNameField = NSTextField()
@@ -69,8 +130,8 @@ final class MyProfilePageView: NSView {
     private let referralField = NSTextField()
     private let referralField = NSTextField()
     private let saveButton = ProfilePrimaryButton(title: "Save Profile  →", 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 nameEmailRow: ProfileDualFieldRow!
+    private var phoneJobRow: ProfileDualFieldRow!
 
 
     private let workExperienceRowsStack = NSStackView()
     private let workExperienceRowsStack = NSStackView()
     private var workExperienceEntries: [WorkExperienceEntryView] = []
     private var workExperienceEntries: [WorkExperienceEntryView] = []
@@ -97,16 +158,6 @@ final class MyProfilePageView: NSView {
         setup()
         setup()
     }
     }
 
 
-    override func updateConstraints() {
-        updateReadableFormCardWidthConstraint()
-        super.updateConstraints()
-    }
-
-    override func viewDidMoveToWindow() {
-        super.viewDidMoveToWindow()
-        needsUpdateConstraints = true
-    }
-
     override func layout() {
     override func layout() {
         super.layout()
         super.layout()
         if let layer = cardView.layer, layer.shadowOpacity > 0 {
         if let layer = cardView.layer, layer.shadowOpacity > 0 {
@@ -117,18 +168,6 @@ final class MyProfilePageView: NSView {
         applyResponsiveRowsIfNeeded()
         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.
     /// 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() {
     private func updateMultilinePreferredLayoutWidths() {
         let horizontalInset: CGFloat = 24
         let horizontalInset: CGFloat = 24
@@ -202,9 +241,6 @@ final class MyProfilePageView: NSView {
             cardView.layer?.cornerCurve = .continuous
             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.translatesAutoresizingMaskIntoConstraints = false
         formStack.orientation = .vertical
         formStack.orientation = .vertical
         formStack.alignment = .width
         formStack.alignment = .width
@@ -234,44 +270,43 @@ final class MyProfilePageView: NSView {
             documentView.topAnchor.constraint(equalTo: scrollView.contentView.topAnchor),
             documentView.topAnchor.constraint(equalTo: scrollView.contentView.topAnchor),
             documentView.bottomAnchor.constraint(equalTo: cardView.bottomAnchor, constant: Self.horizontalPageInset),
             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),
+            // 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),
             cardView.topAnchor.constraint(equalTo: documentView.topAnchor, constant: Self.horizontalPageInset),
-            readableFormWidthConstraint,
 
 
-            formStack.leadingAnchor.constraint(equalTo: cardView.leadingAnchor),
-            formStack.trailingAnchor.constraint(equalTo: cardView.trailingAnchor),
+            formStack.leftAnchor.constraint(equalTo: cardView.leftAnchor),
+            formStack.rightAnchor.constraint(equalTo: cardView.rightAnchor),
             formStack.widthAnchor.constraint(equalTo: cardView.widthAnchor),
             formStack.widthAnchor.constraint(equalTo: cardView.widthAnchor),
             formStack.topAnchor.constraint(equalTo: cardView.topAnchor),
             formStack.topAnchor.constraint(equalTo: cardView.topAnchor),
             formStack.bottomAnchor.constraint(equalTo: cardView.bottomAnchor)
             formStack.bottomAnchor.constraint(equalTo: cardView.bottomAnchor)
         ])
         ])
 
 
-        formStack.addArrangedSubview(labeledGroup(title: "Profile Name *", field: profileNameField, placeholder: "Marketing Director Profile"))
-        formStack.addArrangedSubview(sectionHeading("Personal Information"))
+        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 nameGroup = labeledGroup(title: "Full Name *", field: fullNameField, placeholder: "John Doe")
         let emailGroup = labeledGroup(title: "Email *", field: emailField, placeholder: "john@example.com")
         let emailGroup = labeledGroup(title: "Email *", field: emailField, placeholder: "john@example.com")
-        configureTwoColumnRow(nameEmailRow, left: nameGroup, right: emailGroup)
-        pinEqualColumnWidths(in: nameEmailRow)
-        formStack.addArrangedSubview(nameEmailRow)
+        nameEmailRow = ProfileDualFieldRow(left: nameGroup, right: emailGroup, spacing: 12)
+        addFullWidthArrangedSubview(nameEmailRow)
 
 
         let phoneGroup = labeledGroup(title: "Phone", field: phoneField, placeholder: "+1 (555) 123-4567")
         let phoneGroup = labeledGroup(title: "Phone", field: phoneField, placeholder: "+1 (555) 123-4567")
         let jobGroup = labeledGroup(title: "Job Title *", field: jobTitleField, placeholder: "Software Engineer")
         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(
+        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(
             multilineProfileBlock(
                 title: "Certificates / Rewards",
                 title: "Certificates / Rewards",
                 placeholder: "List your certificates and awards...",
                 placeholder: "List your certificates and awards...",
@@ -279,8 +314,8 @@ final class MyProfilePageView: NSView {
                 minHeight: 100
                 minHeight: 100
             )
             )
         )
         )
-        formStack.addArrangedSubview(horizontalSeparator())
-        formStack.addArrangedSubview(
+        addFullWidthArrangedSubview(horizontalSeparator())
+        addFullWidthArrangedSubview(
             multilineProfileBlock(
             multilineProfileBlock(
                 title: "Interests",
                 title: "Interests",
                 placeholder: "List your interests and hobbies...",
                 placeholder: "List your interests and hobbies...",
@@ -288,8 +323,8 @@ final class MyProfilePageView: NSView {
                 minHeight: 100
                 minHeight: 100
             )
             )
         )
         )
-        formStack.addArrangedSubview(horizontalSeparator())
-        formStack.addArrangedSubview(
+        addFullWidthArrangedSubview(horizontalSeparator())
+        addFullWidthArrangedSubview(
             multilineProfileBlock(
             multilineProfileBlock(
                 title: "Languages",
                 title: "Languages",
                 placeholder: "List languages you speak (e.g., English - Native, Spanish - Fluent)...",
                 placeholder: "List languages you speak (e.g., English - Native, Spanish - Fluent)...",
@@ -297,9 +332,9 @@ final class MyProfilePageView: NSView {
                 minHeight: 100
                 minHeight: 100
             )
             )
         )
         )
-        formStack.addArrangedSubview(horizontalSeparator())
-        formStack.addArrangedSubview(referralBlock())
-        formStack.addArrangedSubview(saveButtonHost())
+        addFullWidthArrangedSubview(horizontalSeparator())
+        addFullWidthArrangedSubview(referralBlock())
+        addFullWidthArrangedSubview(saveButtonHost())
 
 
         saveButton.target = self
         saveButton.target = self
         saveButton.action = #selector(didTapSave)
         saveButton.action = #selector(didTapSave)
@@ -315,14 +350,8 @@ final class MyProfilePageView: NSView {
         let compact = formWidth < Self.compactFormWidth
         let compact = formWidth < Self.compactFormWidth
         guard compact != lastCompactLayout else { return }
         guard compact != lastCompactLayout else { return }
         lastCompactLayout = compact
         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
+        nameEmailRow.setCompact(compact)
+        phoneJobRow.setCompact(compact)
         for entry in workExperienceEntries {
         for entry in workExperienceEntries {
             entry.applyCompactLayout(compact)
             entry.applyCompactLayout(compact)
         }
         }
@@ -331,26 +360,10 @@ final class MyProfilePageView: NSView {
         }
         }
     }
     }
 
 
-    /// 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 addFullWidthArrangedSubview(_ view: NSView) {
+        formStack.addArrangedSubview(view)
+        view.setContentHuggingPriority(.defaultLow, for: .horizontal)
+        view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
     }
     }
 
 
     private func sectionHeading(_ text: String) -> NSView {
     private func sectionHeading(_ text: String) -> NSView {
@@ -374,6 +387,9 @@ final class MyProfilePageView: NSView {
         row.userInterfaceLayoutDirection = .leftToRight
         row.userInterfaceLayoutDirection = .leftToRight
         ProfileLayoutEnforcement.applyForcedLTR(to: row)
         ProfileLayoutEnforcement.applyForcedLTR(to: row)
         row.translatesAutoresizingMaskIntoConstraints = false
         row.translatesAutoresizingMaskIntoConstraints = false
+        NSLayoutConstraint.activate([
+            label.leftAnchor.constraint(equalTo: row.leftAnchor)
+        ])
         return row
         return row
     }
     }
 
 
@@ -400,7 +416,8 @@ final class MyProfilePageView: NSView {
         wrap.setContentHuggingPriority(.defaultLow, for: .horizontal)
         wrap.setContentHuggingPriority(.defaultLow, for: .horizontal)
         wrap.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
         wrap.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
         NSLayoutConstraint.activate([
         NSLayoutConstraint.activate([
-            wrap.widthAnchor.constraint(equalTo: stack.widthAnchor)
+            wrap.widthAnchor.constraint(equalTo: stack.widthAnchor),
+            label.leftAnchor.constraint(equalTo: stack.leftAnchor)
         ])
         ])
         return stack
         return stack
     }
     }
@@ -512,6 +529,9 @@ final class MyProfilePageView: NSView {
         ProfileLayoutEnforcement.applyForcedLTR(to: stack)
         ProfileLayoutEnforcement.applyForcedLTR(to: stack)
         stack.setContentHuggingPriority(.defaultLow, for: .horizontal)
         stack.setContentHuggingPriority(.defaultLow, for: .horizontal)
         wrap.setContentHuggingPriority(.defaultLow, for: .horizontal)
         wrap.setContentHuggingPriority(.defaultLow, for: .horizontal)
+        NSLayoutConstraint.activate([
+            label.leftAnchor.constraint(equalTo: stack.leftAnchor)
+        ])
         return stack
         return stack
     }
     }
 
 
@@ -576,6 +596,9 @@ final class MyProfilePageView: NSView {
         ProfileLayoutEnforcement.applyForcedLTR(to: stack)
         ProfileLayoutEnforcement.applyForcedLTR(to: stack)
         stack.setContentHuggingPriority(.defaultLow, for: .horizontal)
         stack.setContentHuggingPriority(.defaultLow, for: .horizontal)
         wrap.setContentHuggingPriority(.defaultLow, for: .horizontal)
         wrap.setContentHuggingPriority(.defaultLow, for: .horizontal)
+        NSLayoutConstraint.activate([
+            label.leftAnchor.constraint(equalTo: stack.leftAnchor)
+        ])
         return stack
         return stack
     }
     }
 
 
@@ -608,6 +631,10 @@ final class MyProfilePageView: NSView {
         if #available(macOS 10.11, *) {
         if #available(macOS 10.11, *) {
             stack.setCustomSpacing(6, after: wrap)
             stack.setCustomSpacing(6, after: wrap)
         }
         }
+        NSLayoutConstraint.activate([
+            label.leftAnchor.constraint(equalTo: stack.leftAnchor),
+            helper.leftAnchor.constraint(equalTo: stack.leftAnchor)
+        ])
         return stack
         return stack
     }
     }
 
 
@@ -826,7 +853,7 @@ private final class WorkExperienceEntryView: NSView {
     private let companyField = NSTextField()
     private let companyField = NSTextField()
     private let durationField = NSTextField()
     private let durationField = NSTextField()
     private let descriptionField = NSTextField()
     private let descriptionField = NSTextField()
-    private let jobCompanyRow = NSStackView()
+    private var jobCompanyRow: ProfileDualFieldRow!
 
 
     override init(frame frameRect: NSRect) {
     override init(frame frameRect: NSRect) {
         super.init(frame: frameRect)
         super.init(frame: frameRect)
@@ -846,9 +873,7 @@ private final class WorkExperienceEntryView: NSView {
     }
     }
 
 
     func applyCompactLayout(_ compact: Bool) {
     func applyCompactLayout(_ compact: Bool) {
-        jobCompanyRow.orientation = compact ? .vertical : .horizontal
-        jobCompanyRow.spacing = compact ? 12 : 12
-        jobCompanyRow.distribution = compact ? .fill : .fillEqually
+        jobCompanyRow.setCompact(compact)
     }
     }
 
 
     private func configure() {
     private func configure() {
@@ -902,8 +927,7 @@ private final class WorkExperienceEntryView: NSView {
 
 
         let jobGroup = Self.labeledFieldStack(title: "Job Title *", field: jobTitleField, placeholder: "e.g., Software Engineer")
         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")
         let companyGroup = Self.labeledFieldStack(title: "Company Name *", field: companyField, placeholder: "e.g., Google")
-        configureTwoColumnRow(jobCompanyRow, left: jobGroup, right: companyGroup)
-        pinEqualColumnWidths(in: jobCompanyRow)
+        jobCompanyRow = ProfileDualFieldRow(left: jobGroup, right: companyGroup, spacing: 12)
 
 
         let durationGroup = Self.labeledFieldStack(title: "Duration *", field: durationField, placeholder: "e.g., Jan 2020 - Present")
         let durationGroup = Self.labeledFieldStack(title: "Duration *", field: durationField, placeholder: "e.g., Jan 2020 - Present")
         let descriptionGroup = Self.multilineLabeledStack(
         let descriptionGroup = Self.multilineLabeledStack(
@@ -931,20 +955,6 @@ private final class WorkExperienceEntryView: NSView {
         ])
         ])
     }
     }
 
 
-    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() {
     override func layout() {
         super.layout()
         super.layout()
         for field in [jobTitleField, companyField, durationField, descriptionField] {
         for field in [jobTitleField, companyField, durationField, descriptionField] {
@@ -960,13 +970,6 @@ private final class WorkExperienceEntryView: NSView {
         layer.shadowPath = CGPath(roundedRect: bounds, cornerWidth: r, cornerHeight: r, transform: nil)
         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() {
     @objc private func didTapDelete() {
         onDelete?()
         onDelete?()
     }
     }
@@ -988,7 +991,13 @@ private final class WorkExperienceEntryView: NSView {
         stack.translatesAutoresizingMaskIntoConstraints = false
         stack.translatesAutoresizingMaskIntoConstraints = false
         stack.userInterfaceLayoutDirection = .leftToRight
         stack.userInterfaceLayoutDirection = .leftToRight
         ProfileLayoutEnforcement.applyForcedLTR(to: stack)
         ProfileLayoutEnforcement.applyForcedLTR(to: stack)
-        NSLayoutConstraint.activate([wrap.widthAnchor.constraint(equalTo: stack.widthAnchor)])
+        stack.setContentHuggingPriority(.defaultLow, for: .horizontal)
+        wrap.setContentHuggingPriority(.defaultLow, for: .horizontal)
+        wrap.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
+        NSLayoutConstraint.activate([
+            wrap.widthAnchor.constraint(equalTo: stack.widthAnchor),
+            label.leftAnchor.constraint(equalTo: stack.leftAnchor)
+        ])
         return stack
         return stack
     }
     }
 
 
@@ -1048,6 +1057,11 @@ private final class WorkExperienceEntryView: NSView {
         stack.translatesAutoresizingMaskIntoConstraints = false
         stack.translatesAutoresizingMaskIntoConstraints = false
         stack.userInterfaceLayoutDirection = .leftToRight
         stack.userInterfaceLayoutDirection = .leftToRight
         ProfileLayoutEnforcement.applyForcedLTR(to: stack)
         ProfileLayoutEnforcement.applyForcedLTR(to: stack)
+        stack.setContentHuggingPriority(.defaultLow, for: .horizontal)
+        wrap.setContentHuggingPriority(.defaultLow, for: .horizontal)
+        NSLayoutConstraint.activate([
+            label.leftAnchor.constraint(equalTo: stack.leftAnchor)
+        ])
         return stack
         return stack
     }
     }
 
 
@@ -1103,7 +1117,7 @@ private final class EducationEntryView: NSView {
     private let degreeField = NSTextField()
     private let degreeField = NSTextField()
     private let institutionField = NSTextField()
     private let institutionField = NSTextField()
     private let yearField = NSTextField()
     private let yearField = NSTextField()
-    private let degreeInstitutionRow = NSStackView()
+    private var degreeInstitutionRow: ProfileDualFieldRow!
 
 
     override init(frame frameRect: NSRect) {
     override init(frame frameRect: NSRect) {
         super.init(frame: frameRect)
         super.init(frame: frameRect)
@@ -1123,9 +1137,7 @@ private final class EducationEntryView: NSView {
     }
     }
 
 
     func applyCompactLayout(_ compact: Bool) {
     func applyCompactLayout(_ compact: Bool) {
-        degreeInstitutionRow.orientation = compact ? .vertical : .horizontal
-        degreeInstitutionRow.spacing = 12
-        degreeInstitutionRow.distribution = compact ? .fill : .fillEqually
+        degreeInstitutionRow.setCompact(compact)
     }
     }
 
 
     private func configure() {
     private func configure() {
@@ -1187,8 +1199,7 @@ private final class EducationEntryView: NSView {
             field: institutionField,
             field: institutionField,
             placeholder: "e.g., MIT"
             placeholder: "e.g., MIT"
         )
         )
-        configureTwoColumnRow(degreeInstitutionRow, left: degreeGroup, right: institutionGroup)
-        pinEqualColumnWidths(in: degreeInstitutionRow)
+        degreeInstitutionRow = ProfileDualFieldRow(left: degreeGroup, right: institutionGroup, spacing: 12)
 
 
         let yearGroup = WorkExperienceEntryView.labeledFieldStack(
         let yearGroup = WorkExperienceEntryView.labeledFieldStack(
             title: "Year *",
             title: "Year *",
@@ -1214,34 +1225,21 @@ private final class EducationEntryView: NSView {
         ])
         ])
     }
     }
 
 
-    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() {
     override func layout() {
         super.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 }
         guard let layer = layer, layer.shadowOpacity > 0 else { return }
         let r = layer.cornerRadius
         let r = layer.cornerRadius
         layer.shadowPath = CGPath(roundedRect: bounds, cornerWidth: r, cornerHeight: r, transform: nil)
         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() {
     @objc private func didTapDelete() {
         onDelete?()
         onDelete?()
     }
     }