Преглед изворни кода

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 пре 3 недеља
родитељ
комит
3d968e7bfa
1 измењених фајлова са 143 додато и 145 уклоњено
  1. 143 145
      App for Indeed/Views/MyProfilePageView.swift

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

@@ -44,16 +44,77 @@ private enum ProfileLayoutEnforcement {
44 44
     }
45 45
 }
46 46
 
47
+/// 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.
48
+private final class ProfileDualFieldRow: NSView {
49
+    private let leftView: NSView
50
+    private let rightView: NSView
51
+    private let spacing: CGFloat
52
+    private var horizontalConstraints: [NSLayoutConstraint] = []
53
+    private var verticalConstraints: [NSLayoutConstraint] = []
54
+    private var isCompact = false
55
+
56
+    init(left: NSView, right: NSView, spacing: CGFloat = 12) {
57
+        self.leftView = left
58
+        self.rightView = right
59
+        self.spacing = spacing
60
+        super.init(frame: .zero)
61
+        translatesAutoresizingMaskIntoConstraints = false
62
+        ProfileLayoutEnforcement.applyForcedLTR(to: self)
63
+        addSubview(leftView)
64
+        addSubview(rightView)
65
+        leftView.translatesAutoresizingMaskIntoConstraints = false
66
+        rightView.translatesAutoresizingMaskIntoConstraints = false
67
+        leftView.setContentHuggingPriority(.defaultLow, for: .horizontal)
68
+        rightView.setContentHuggingPriority(.defaultLow, for: .horizontal)
69
+        leftView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
70
+        rightView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
71
+        horizontalConstraints = [
72
+            leftView.leftAnchor.constraint(equalTo: leftAnchor),
73
+            leftView.topAnchor.constraint(equalTo: topAnchor),
74
+            leftView.bottomAnchor.constraint(equalTo: bottomAnchor),
75
+            rightView.rightAnchor.constraint(equalTo: rightAnchor),
76
+            rightView.topAnchor.constraint(equalTo: topAnchor),
77
+            rightView.bottomAnchor.constraint(equalTo: bottomAnchor),
78
+            leftView.rightAnchor.constraint(equalTo: rightView.leftAnchor, constant: -spacing),
79
+            leftView.widthAnchor.constraint(equalTo: rightView.widthAnchor)
80
+        ]
81
+        verticalConstraints = [
82
+            leftView.leftAnchor.constraint(equalTo: leftAnchor),
83
+            leftView.rightAnchor.constraint(equalTo: rightAnchor),
84
+            leftView.topAnchor.constraint(equalTo: topAnchor),
85
+            rightView.leftAnchor.constraint(equalTo: leftAnchor),
86
+            rightView.rightAnchor.constraint(equalTo: rightAnchor),
87
+            rightView.topAnchor.constraint(equalTo: leftView.bottomAnchor, constant: spacing),
88
+            rightView.bottomAnchor.constraint(equalTo: bottomAnchor)
89
+        ]
90
+        NSLayoutConstraint.activate(horizontalConstraints)
91
+    }
92
+
93
+    required init?(coder: NSCoder) {
94
+        fatalError("init(coder:) has not been implemented")
95
+    }
96
+
97
+    func setCompact(_ compact: Bool) {
98
+        guard compact != isCompact else { return }
99
+        isCompact = compact
100
+        if compact {
101
+            NSLayoutConstraint.deactivate(horizontalConstraints)
102
+            NSLayoutConstraint.activate(verticalConstraints)
103
+        } else {
104
+            NSLayoutConstraint.deactivate(verticalConstraints)
105
+            NSLayoutConstraint.activate(horizontalConstraints)
106
+        }
107
+    }
108
+}
109
+
47 110
 final class MyProfilePageView: NSView {
48 111
     /// Below this form content width, two-column rows stack vertically.
49 112
     private static let compactFormWidth: CGFloat = 640
50
-    private static let readableFormMaxWidth: CGFloat = 880
51 113
     private static let horizontalPageInset: CGFloat = 24
52 114
 
53 115
     private let scrollView = NSScrollView()
54 116
     private let documentView = NSView()
55 117
     private let cardView = NSView()
56
-    private var readableFormWidthConstraint: NSLayoutConstraint!
57 118
     private let formStack = NSStackView()
58 119
 
59 120
     private let profileNameField = NSTextField()
@@ -69,8 +130,8 @@ final class MyProfilePageView: NSView {
69 130
     private let referralField = NSTextField()
70 131
     private let saveButton = ProfilePrimaryButton(title: "Save Profile  →", target: nil, action: nil)
71 132
 
72
-    private let nameEmailRow = NSStackView()
73
-    private let phoneJobRow = NSStackView()
133
+    private var nameEmailRow: ProfileDualFieldRow!
134
+    private var phoneJobRow: ProfileDualFieldRow!
74 135
 
75 136
     private let workExperienceRowsStack = NSStackView()
76 137
     private var workExperienceEntries: [WorkExperienceEntryView] = []
@@ -97,16 +158,6 @@ final class MyProfilePageView: NSView {
97 158
         setup()
98 159
     }
99 160
 
100
-    override func updateConstraints() {
101
-        updateReadableFormCardWidthConstraint()
102
-        super.updateConstraints()
103
-    }
104
-
105
-    override func viewDidMoveToWindow() {
106
-        super.viewDidMoveToWindow()
107
-        needsUpdateConstraints = true
108
-    }
109
-
110 161
     override func layout() {
111 162
         super.layout()
112 163
         if let layer = cardView.layer, layer.shadowOpacity > 0 {
@@ -117,18 +168,6 @@ final class MyProfilePageView: NSView {
117 168
         applyResponsiveRowsIfNeeded()
118 169
     }
119 170
 
120
-    /// 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.
121
-    private func updateReadableFormCardWidthConstraint() {
122
-        let clipW = max(bounds.width, scrollView.bounds.width, scrollView.contentView.bounds.width)
123
-        guard clipW > 1 else { return }
124
-        let horizontalCardInset = Self.horizontalPageInset * 2
125
-        let maxUsable = clipW - horizontalCardInset
126
-        let target = min(Self.readableFormMaxWidth, max(1, maxUsable))
127
-        if abs(readableFormWidthConstraint.constant - target) > 0.5 {
128
-            readableFormWidthConstraint.constant = target
129
-        }
130
-    }
131
-
132 171
     /// Wrapping `NSTextField`s report a tiny intrinsic width until `preferredMaxLayoutWidth` tracks the chrome width, which otherwise collapses the stack to a narrow trailing column.
133 172
     private func updateMultilinePreferredLayoutWidths() {
134 173
         let horizontalInset: CGFloat = 24
@@ -202,9 +241,6 @@ final class MyProfilePageView: NSView {
202 241
             cardView.layer?.cornerCurve = .continuous
203 242
         }
204 243
 
205
-        // Start narrow so the first constraint pass cannot exceed a small host; `updateConstraints` sets the real width.
206
-        readableFormWidthConstraint = cardView.widthAnchor.constraint(equalToConstant: 200)
207
-
208 244
         formStack.translatesAutoresizingMaskIntoConstraints = false
209 245
         formStack.orientation = .vertical
210 246
         formStack.alignment = .width
@@ -234,44 +270,43 @@ final class MyProfilePageView: NSView {
234 270
             documentView.topAnchor.constraint(equalTo: scrollView.contentView.topAnchor),
235 271
             documentView.bottomAnchor.constraint(equalTo: cardView.bottomAnchor, constant: Self.horizontalPageInset),
236 272
 
237
-            cardView.leadingAnchor.constraint(equalTo: documentView.leadingAnchor, constant: Self.horizontalPageInset),
238
-            cardView.trailingAnchor.constraint(lessThanOrEqualTo: documentView.trailingAnchor, constant: -Self.horizontalPageInset),
273
+            // 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.
274
+            cardView.leftAnchor.constraint(equalTo: documentView.leftAnchor, constant: Self.horizontalPageInset),
275
+            cardView.rightAnchor.constraint(equalTo: documentView.rightAnchor, constant: -Self.horizontalPageInset),
239 276
             cardView.topAnchor.constraint(equalTo: documentView.topAnchor, constant: Self.horizontalPageInset),
240
-            readableFormWidthConstraint,
241 277
 
242
-            formStack.leadingAnchor.constraint(equalTo: cardView.leadingAnchor),
243
-            formStack.trailingAnchor.constraint(equalTo: cardView.trailingAnchor),
278
+            formStack.leftAnchor.constraint(equalTo: cardView.leftAnchor),
279
+            formStack.rightAnchor.constraint(equalTo: cardView.rightAnchor),
244 280
             formStack.widthAnchor.constraint(equalTo: cardView.widthAnchor),
245 281
             formStack.topAnchor.constraint(equalTo: cardView.topAnchor),
246 282
             formStack.bottomAnchor.constraint(equalTo: cardView.bottomAnchor)
247 283
         ])
248 284
 
249
-        formStack.addArrangedSubview(labeledGroup(title: "Profile Name *", field: profileNameField, placeholder: "Marketing Director Profile"))
250
-        formStack.addArrangedSubview(sectionHeading("Personal Information"))
285
+        addFullWidthArrangedSubview(
286
+            labeledGroup(title: "Profile Name *", field: profileNameField, placeholder: "Marketing Director Profile")
287
+        )
288
+        addFullWidthArrangedSubview(sectionHeading("Personal Information"))
251 289
 
252 290
         let nameGroup = labeledGroup(title: "Full Name *", field: fullNameField, placeholder: "John Doe")
253 291
         let emailGroup = labeledGroup(title: "Email *", field: emailField, placeholder: "john@example.com")
254
-        configureTwoColumnRow(nameEmailRow, left: nameGroup, right: emailGroup)
255
-        pinEqualColumnWidths(in: nameEmailRow)
256
-        formStack.addArrangedSubview(nameEmailRow)
292
+        nameEmailRow = ProfileDualFieldRow(left: nameGroup, right: emailGroup, spacing: 12)
293
+        addFullWidthArrangedSubview(nameEmailRow)
257 294
 
258 295
         let phoneGroup = labeledGroup(title: "Phone", field: phoneField, placeholder: "+1 (555) 123-4567")
259 296
         let jobGroup = labeledGroup(title: "Job Title *", field: jobTitleField, placeholder: "Software Engineer")
260
-        configureTwoColumnRow(phoneJobRow, left: phoneGroup, right: jobGroup)
261
-        pinEqualColumnWidths(in: phoneJobRow)
262
-        formStack.addArrangedSubview(phoneJobRow)
263
-
264
-        ProfileLayoutEnforcement.applyForcedLTR(to: nameEmailRow)
265
-        ProfileLayoutEnforcement.applyForcedLTR(to: phoneJobRow)
266
-
267
-        formStack.addArrangedSubview(labeledGroup(title: "Address", field: addressField, placeholder: "123 Main St, City, State, ZIP"))
268
-        formStack.addArrangedSubview(careerSummaryBlock())
269
-        formStack.addArrangedSubview(horizontalSeparator())
270
-        formStack.addArrangedSubview(workExperienceSection())
271
-        formStack.addArrangedSubview(horizontalSeparator())
272
-        formStack.addArrangedSubview(educationSection())
273
-        formStack.addArrangedSubview(horizontalSeparator())
274
-        formStack.addArrangedSubview(
297
+        phoneJobRow = ProfileDualFieldRow(left: phoneGroup, right: jobGroup, spacing: 12)
298
+        addFullWidthArrangedSubview(phoneJobRow)
299
+
300
+        addFullWidthArrangedSubview(
301
+            labeledGroup(title: "Address", field: addressField, placeholder: "123 Main St, City, State, ZIP")
302
+        )
303
+        addFullWidthArrangedSubview(careerSummaryBlock())
304
+        addFullWidthArrangedSubview(horizontalSeparator())
305
+        addFullWidthArrangedSubview(workExperienceSection())
306
+        addFullWidthArrangedSubview(horizontalSeparator())
307
+        addFullWidthArrangedSubview(educationSection())
308
+        addFullWidthArrangedSubview(horizontalSeparator())
309
+        addFullWidthArrangedSubview(
275 310
             multilineProfileBlock(
276 311
                 title: "Certificates / Rewards",
277 312
                 placeholder: "List your certificates and awards...",
@@ -279,8 +314,8 @@ final class MyProfilePageView: NSView {
279 314
                 minHeight: 100
280 315
             )
281 316
         )
282
-        formStack.addArrangedSubview(horizontalSeparator())
283
-        formStack.addArrangedSubview(
317
+        addFullWidthArrangedSubview(horizontalSeparator())
318
+        addFullWidthArrangedSubview(
284 319
             multilineProfileBlock(
285 320
                 title: "Interests",
286 321
                 placeholder: "List your interests and hobbies...",
@@ -288,8 +323,8 @@ final class MyProfilePageView: NSView {
288 323
                 minHeight: 100
289 324
             )
290 325
         )
291
-        formStack.addArrangedSubview(horizontalSeparator())
292
-        formStack.addArrangedSubview(
326
+        addFullWidthArrangedSubview(horizontalSeparator())
327
+        addFullWidthArrangedSubview(
293 328
             multilineProfileBlock(
294 329
                 title: "Languages",
295 330
                 placeholder: "List languages you speak (e.g., English - Native, Spanish - Fluent)...",
@@ -297,9 +332,9 @@ final class MyProfilePageView: NSView {
297 332
                 minHeight: 100
298 333
             )
299 334
         )
300
-        formStack.addArrangedSubview(horizontalSeparator())
301
-        formStack.addArrangedSubview(referralBlock())
302
-        formStack.addArrangedSubview(saveButtonHost())
335
+        addFullWidthArrangedSubview(horizontalSeparator())
336
+        addFullWidthArrangedSubview(referralBlock())
337
+        addFullWidthArrangedSubview(saveButtonHost())
303 338
 
304 339
         saveButton.target = self
305 340
         saveButton.action = #selector(didTapSave)
@@ -315,14 +350,8 @@ final class MyProfilePageView: NSView {
315 350
         let compact = formWidth < Self.compactFormWidth
316 351
         guard compact != lastCompactLayout else { return }
317 352
         lastCompactLayout = compact
318
-        let orientation: NSUserInterfaceLayoutOrientation = compact ? .vertical : .horizontal
319
-        let rowSpacing: CGFloat = compact ? 16 : 12
320
-        nameEmailRow.orientation = orientation
321
-        nameEmailRow.spacing = rowSpacing
322
-        phoneJobRow.orientation = orientation
323
-        phoneJobRow.spacing = rowSpacing
324
-        nameEmailRow.distribution = compact ? .fill : .fillEqually
325
-        phoneJobRow.distribution = compact ? .fill : .fillEqually
353
+        nameEmailRow.setCompact(compact)
354
+        phoneJobRow.setCompact(compact)
326 355
         for entry in workExperienceEntries {
327 356
             entry.applyCompactLayout(compact)
328 357
         }
@@ -331,26 +360,10 @@ final class MyProfilePageView: NSView {
331 360
         }
332 361
     }
333 362
 
334
-    /// Keeps two columns the same width when the row is horizontal; avoids NSTextField intrinsic width fighting the stack.
335
-    private func pinEqualColumnWidths(in row: NSStackView) {
336
-        guard row.arrangedSubviews.count == 2 else { return }
337
-        let left = row.arrangedSubviews[0]
338
-        let right = row.arrangedSubviews[1]
339
-        left.widthAnchor.constraint(equalTo: right.widthAnchor).isActive = true
340
-    }
341
-
342
-    private func configureTwoColumnRow(_ row: NSStackView, left: NSView, right: NSView) {
343
-        row.translatesAutoresizingMaskIntoConstraints = false
344
-        row.orientation = .horizontal
345
-        row.spacing = 12
346
-        row.distribution = .fillEqually
347
-        row.alignment = .top
348
-        row.userInterfaceLayoutDirection = .leftToRight
349
-        ProfileLayoutEnforcement.applyForcedLTR(to: row)
350
-        row.setContentHuggingPriority(.defaultLow, for: .horizontal)
351
-        row.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
352
-        row.addArrangedSubview(left)
353
-        row.addArrangedSubview(right)
363
+    private func addFullWidthArrangedSubview(_ view: NSView) {
364
+        formStack.addArrangedSubview(view)
365
+        view.setContentHuggingPriority(.defaultLow, for: .horizontal)
366
+        view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
354 367
     }
355 368
 
356 369
     private func sectionHeading(_ text: String) -> NSView {
@@ -374,6 +387,9 @@ final class MyProfilePageView: NSView {
374 387
         row.userInterfaceLayoutDirection = .leftToRight
375 388
         ProfileLayoutEnforcement.applyForcedLTR(to: row)
376 389
         row.translatesAutoresizingMaskIntoConstraints = false
390
+        NSLayoutConstraint.activate([
391
+            label.leftAnchor.constraint(equalTo: row.leftAnchor)
392
+        ])
377 393
         return row
378 394
     }
379 395
 
@@ -400,7 +416,8 @@ final class MyProfilePageView: NSView {
400 416
         wrap.setContentHuggingPriority(.defaultLow, for: .horizontal)
401 417
         wrap.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
402 418
         NSLayoutConstraint.activate([
403
-            wrap.widthAnchor.constraint(equalTo: stack.widthAnchor)
419
+            wrap.widthAnchor.constraint(equalTo: stack.widthAnchor),
420
+            label.leftAnchor.constraint(equalTo: stack.leftAnchor)
404 421
         ])
405 422
         return stack
406 423
     }
@@ -512,6 +529,9 @@ final class MyProfilePageView: NSView {
512 529
         ProfileLayoutEnforcement.applyForcedLTR(to: stack)
513 530
         stack.setContentHuggingPriority(.defaultLow, for: .horizontal)
514 531
         wrap.setContentHuggingPriority(.defaultLow, for: .horizontal)
532
+        NSLayoutConstraint.activate([
533
+            label.leftAnchor.constraint(equalTo: stack.leftAnchor)
534
+        ])
515 535
         return stack
516 536
     }
517 537
 
@@ -576,6 +596,9 @@ final class MyProfilePageView: NSView {
576 596
         ProfileLayoutEnforcement.applyForcedLTR(to: stack)
577 597
         stack.setContentHuggingPriority(.defaultLow, for: .horizontal)
578 598
         wrap.setContentHuggingPriority(.defaultLow, for: .horizontal)
599
+        NSLayoutConstraint.activate([
600
+            label.leftAnchor.constraint(equalTo: stack.leftAnchor)
601
+        ])
579 602
         return stack
580 603
     }
581 604
 
@@ -608,6 +631,10 @@ final class MyProfilePageView: NSView {
608 631
         if #available(macOS 10.11, *) {
609 632
             stack.setCustomSpacing(6, after: wrap)
610 633
         }
634
+        NSLayoutConstraint.activate([
635
+            label.leftAnchor.constraint(equalTo: stack.leftAnchor),
636
+            helper.leftAnchor.constraint(equalTo: stack.leftAnchor)
637
+        ])
611 638
         return stack
612 639
     }
613 640
 
@@ -826,7 +853,7 @@ private final class WorkExperienceEntryView: NSView {
826 853
     private let companyField = NSTextField()
827 854
     private let durationField = NSTextField()
828 855
     private let descriptionField = NSTextField()
829
-    private let jobCompanyRow = NSStackView()
856
+    private var jobCompanyRow: ProfileDualFieldRow!
830 857
 
831 858
     override init(frame frameRect: NSRect) {
832 859
         super.init(frame: frameRect)
@@ -846,9 +873,7 @@ private final class WorkExperienceEntryView: NSView {
846 873
     }
847 874
 
848 875
     func applyCompactLayout(_ compact: Bool) {
849
-        jobCompanyRow.orientation = compact ? .vertical : .horizontal
850
-        jobCompanyRow.spacing = compact ? 12 : 12
851
-        jobCompanyRow.distribution = compact ? .fill : .fillEqually
876
+        jobCompanyRow.setCompact(compact)
852 877
     }
853 878
 
854 879
     private func configure() {
@@ -902,8 +927,7 @@ private final class WorkExperienceEntryView: NSView {
902 927
 
903 928
         let jobGroup = Self.labeledFieldStack(title: "Job Title *", field: jobTitleField, placeholder: "e.g., Software Engineer")
904 929
         let companyGroup = Self.labeledFieldStack(title: "Company Name *", field: companyField, placeholder: "e.g., Google")
905
-        configureTwoColumnRow(jobCompanyRow, left: jobGroup, right: companyGroup)
906
-        pinEqualColumnWidths(in: jobCompanyRow)
930
+        jobCompanyRow = ProfileDualFieldRow(left: jobGroup, right: companyGroup, spacing: 12)
907 931
 
908 932
         let durationGroup = Self.labeledFieldStack(title: "Duration *", field: durationField, placeholder: "e.g., Jan 2020 - Present")
909 933
         let descriptionGroup = Self.multilineLabeledStack(
@@ -931,20 +955,6 @@ private final class WorkExperienceEntryView: NSView {
931 955
         ])
932 956
     }
933 957
 
934
-    private func configureTwoColumnRow(_ row: NSStackView, left: NSView, right: NSView) {
935
-        row.translatesAutoresizingMaskIntoConstraints = false
936
-        row.orientation = .horizontal
937
-        row.spacing = 12
938
-        row.distribution = .fillEqually
939
-        row.alignment = .top
940
-        row.userInterfaceLayoutDirection = .leftToRight
941
-        ProfileLayoutEnforcement.applyForcedLTR(to: row)
942
-        row.setContentHuggingPriority(.defaultLow, for: .horizontal)
943
-        row.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
944
-        row.addArrangedSubview(left)
945
-        row.addArrangedSubview(right)
946
-    }
947
-
948 958
     override func layout() {
949 959
         super.layout()
950 960
         for field in [jobTitleField, companyField, durationField, descriptionField] {
@@ -960,13 +970,6 @@ private final class WorkExperienceEntryView: NSView {
960 970
         layer.shadowPath = CGPath(roundedRect: bounds, cornerWidth: r, cornerHeight: r, transform: nil)
961 971
     }
962 972
 
963
-    private func pinEqualColumnWidths(in row: NSStackView) {
964
-        guard row.arrangedSubviews.count == 2 else { return }
965
-        let left = row.arrangedSubviews[0]
966
-        let right = row.arrangedSubviews[1]
967
-        left.widthAnchor.constraint(equalTo: right.widthAnchor).isActive = true
968
-    }
969
-
970 973
     @objc private func didTapDelete() {
971 974
         onDelete?()
972 975
     }
@@ -988,7 +991,13 @@ private final class WorkExperienceEntryView: NSView {
988 991
         stack.translatesAutoresizingMaskIntoConstraints = false
989 992
         stack.userInterfaceLayoutDirection = .leftToRight
990 993
         ProfileLayoutEnforcement.applyForcedLTR(to: stack)
991
-        NSLayoutConstraint.activate([wrap.widthAnchor.constraint(equalTo: stack.widthAnchor)])
994
+        stack.setContentHuggingPriority(.defaultLow, for: .horizontal)
995
+        wrap.setContentHuggingPriority(.defaultLow, for: .horizontal)
996
+        wrap.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
997
+        NSLayoutConstraint.activate([
998
+            wrap.widthAnchor.constraint(equalTo: stack.widthAnchor),
999
+            label.leftAnchor.constraint(equalTo: stack.leftAnchor)
1000
+        ])
992 1001
         return stack
993 1002
     }
994 1003
 
@@ -1048,6 +1057,11 @@ private final class WorkExperienceEntryView: NSView {
1048 1057
         stack.translatesAutoresizingMaskIntoConstraints = false
1049 1058
         stack.userInterfaceLayoutDirection = .leftToRight
1050 1059
         ProfileLayoutEnforcement.applyForcedLTR(to: stack)
1060
+        stack.setContentHuggingPriority(.defaultLow, for: .horizontal)
1061
+        wrap.setContentHuggingPriority(.defaultLow, for: .horizontal)
1062
+        NSLayoutConstraint.activate([
1063
+            label.leftAnchor.constraint(equalTo: stack.leftAnchor)
1064
+        ])
1051 1065
         return stack
1052 1066
     }
1053 1067
 
@@ -1103,7 +1117,7 @@ private final class EducationEntryView: NSView {
1103 1117
     private let degreeField = NSTextField()
1104 1118
     private let institutionField = NSTextField()
1105 1119
     private let yearField = NSTextField()
1106
-    private let degreeInstitutionRow = NSStackView()
1120
+    private var degreeInstitutionRow: ProfileDualFieldRow!
1107 1121
 
1108 1122
     override init(frame frameRect: NSRect) {
1109 1123
         super.init(frame: frameRect)
@@ -1123,9 +1137,7 @@ private final class EducationEntryView: NSView {
1123 1137
     }
1124 1138
 
1125 1139
     func applyCompactLayout(_ compact: Bool) {
1126
-        degreeInstitutionRow.orientation = compact ? .vertical : .horizontal
1127
-        degreeInstitutionRow.spacing = 12
1128
-        degreeInstitutionRow.distribution = compact ? .fill : .fillEqually
1140
+        degreeInstitutionRow.setCompact(compact)
1129 1141
     }
1130 1142
 
1131 1143
     private func configure() {
@@ -1187,8 +1199,7 @@ private final class EducationEntryView: NSView {
1187 1199
             field: institutionField,
1188 1200
             placeholder: "e.g., MIT"
1189 1201
         )
1190
-        configureTwoColumnRow(degreeInstitutionRow, left: degreeGroup, right: institutionGroup)
1191
-        pinEqualColumnWidths(in: degreeInstitutionRow)
1202
+        degreeInstitutionRow = ProfileDualFieldRow(left: degreeGroup, right: institutionGroup, spacing: 12)
1192 1203
 
1193 1204
         let yearGroup = WorkExperienceEntryView.labeledFieldStack(
1194 1205
             title: "Year *",
@@ -1214,34 +1225,21 @@ private final class EducationEntryView: NSView {
1214 1225
         ])
1215 1226
     }
1216 1227
 
1217
-    private func configureTwoColumnRow(_ row: NSStackView, left: NSView, right: NSView) {
1218
-        row.translatesAutoresizingMaskIntoConstraints = false
1219
-        row.orientation = .horizontal
1220
-        row.spacing = 12
1221
-        row.distribution = .fillEqually
1222
-        row.alignment = .top
1223
-        row.userInterfaceLayoutDirection = .leftToRight
1224
-        ProfileLayoutEnforcement.applyForcedLTR(to: row)
1225
-        row.setContentHuggingPriority(.defaultLow, for: .horizontal)
1226
-        row.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
1227
-        row.addArrangedSubview(left)
1228
-        row.addArrangedSubview(right)
1229
-    }
1230
-
1231 1228
     override func layout() {
1232 1229
         super.layout()
1230
+        for field in [degreeField, institutionField, yearField] {
1231
+            if let wrap = field.superview, wrap.bounds.width > 2 {
1232
+                let w = max(1, wrap.bounds.width - 24)
1233
+                if abs(field.preferredMaxLayoutWidth - w) > 0.5 {
1234
+                    field.preferredMaxLayoutWidth = w
1235
+                }
1236
+            }
1237
+        }
1233 1238
         guard let layer = layer, layer.shadowOpacity > 0 else { return }
1234 1239
         let r = layer.cornerRadius
1235 1240
         layer.shadowPath = CGPath(roundedRect: bounds, cornerWidth: r, cornerHeight: r, transform: nil)
1236 1241
     }
1237 1242
 
1238
-    private func pinEqualColumnWidths(in row: NSStackView) {
1239
-        guard row.arrangedSubviews.count == 2 else { return }
1240
-        let left = row.arrangedSubviews[0]
1241
-        let right = row.arrangedSubviews[1]
1242
-        left.widthAnchor.constraint(equalTo: right.widthAnchor).isActive = true
1243
-    }
1244
-
1245 1243
     @objc private func didTapDelete() {
1246 1244
         onDelete?()
1247 1245
     }