瀏覽代碼

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
 final class MyProfilePageView: NSView {
110
 final class MyProfilePageView: NSView {
48
     /// Below this form content width, two-column rows stack vertically.
111
     /// Below this form content width, two-column rows stack vertically.
49
     private static let compactFormWidth: CGFloat = 640
112
     private static let compactFormWidth: CGFloat = 640
50
-    private static let readableFormMaxWidth: CGFloat = 880
51
     private static let horizontalPageInset: CGFloat = 24
113
     private static let horizontalPageInset: CGFloat = 24
52
 
114
 
53
     private let scrollView = NSScrollView()
115
     private let scrollView = NSScrollView()
54
     private let documentView = NSView()
116
     private let documentView = NSView()
55
     private let cardView = NSView()
117
     private let cardView = NSView()
56
-    private var readableFormWidthConstraint: NSLayoutConstraint!
57
     private let formStack = NSStackView()
118
     private let formStack = NSStackView()
58
 
119
 
59
     private let profileNameField = NSTextField()
120
     private let profileNameField = NSTextField()
@@ -69,8 +130,8 @@ final class MyProfilePageView: NSView {
69
     private let referralField = NSTextField()
130
     private let referralField = NSTextField()
70
     private let saveButton = ProfilePrimaryButton(title: "Save Profile  →", target: nil, action: nil)
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
     private let workExperienceRowsStack = NSStackView()
136
     private let workExperienceRowsStack = NSStackView()
76
     private var workExperienceEntries: [WorkExperienceEntryView] = []
137
     private var workExperienceEntries: [WorkExperienceEntryView] = []
@@ -97,16 +158,6 @@ final class MyProfilePageView: NSView {
97
         setup()
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
     override func layout() {
161
     override func layout() {
111
         super.layout()
162
         super.layout()
112
         if let layer = cardView.layer, layer.shadowOpacity > 0 {
163
         if let layer = cardView.layer, layer.shadowOpacity > 0 {
@@ -117,18 +168,6 @@ final class MyProfilePageView: NSView {
117
         applyResponsiveRowsIfNeeded()
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
     /// Wrapping `NSTextField`s report a tiny intrinsic width until `preferredMaxLayoutWidth` tracks the chrome width, which otherwise collapses the stack to a narrow trailing column.
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
     private func updateMultilinePreferredLayoutWidths() {
172
     private func updateMultilinePreferredLayoutWidths() {
134
         let horizontalInset: CGFloat = 24
173
         let horizontalInset: CGFloat = 24
@@ -202,9 +241,6 @@ final class MyProfilePageView: NSView {
202
             cardView.layer?.cornerCurve = .continuous
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
         formStack.translatesAutoresizingMaskIntoConstraints = false
244
         formStack.translatesAutoresizingMaskIntoConstraints = false
209
         formStack.orientation = .vertical
245
         formStack.orientation = .vertical
210
         formStack.alignment = .width
246
         formStack.alignment = .width
@@ -234,44 +270,43 @@ final class MyProfilePageView: NSView {
234
             documentView.topAnchor.constraint(equalTo: scrollView.contentView.topAnchor),
270
             documentView.topAnchor.constraint(equalTo: scrollView.contentView.topAnchor),
235
             documentView.bottomAnchor.constraint(equalTo: cardView.bottomAnchor, constant: Self.horizontalPageInset),
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
             cardView.topAnchor.constraint(equalTo: documentView.topAnchor, constant: Self.horizontalPageInset),
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
             formStack.widthAnchor.constraint(equalTo: cardView.widthAnchor),
280
             formStack.widthAnchor.constraint(equalTo: cardView.widthAnchor),
245
             formStack.topAnchor.constraint(equalTo: cardView.topAnchor),
281
             formStack.topAnchor.constraint(equalTo: cardView.topAnchor),
246
             formStack.bottomAnchor.constraint(equalTo: cardView.bottomAnchor)
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
         let nameGroup = labeledGroup(title: "Full Name *", field: fullNameField, placeholder: "John Doe")
290
         let nameGroup = labeledGroup(title: "Full Name *", field: fullNameField, placeholder: "John Doe")
253
         let emailGroup = labeledGroup(title: "Email *", field: emailField, placeholder: "john@example.com")
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
         let phoneGroup = labeledGroup(title: "Phone", field: phoneField, placeholder: "+1 (555) 123-4567")
295
         let phoneGroup = labeledGroup(title: "Phone", field: phoneField, placeholder: "+1 (555) 123-4567")
259
         let jobGroup = labeledGroup(title: "Job Title *", field: jobTitleField, placeholder: "Software Engineer")
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
             multilineProfileBlock(
310
             multilineProfileBlock(
276
                 title: "Certificates / Rewards",
311
                 title: "Certificates / Rewards",
277
                 placeholder: "List your certificates and awards...",
312
                 placeholder: "List your certificates and awards...",
@@ -279,8 +314,8 @@ final class MyProfilePageView: NSView {
279
                 minHeight: 100
314
                 minHeight: 100
280
             )
315
             )
281
         )
316
         )
282
-        formStack.addArrangedSubview(horizontalSeparator())
283
-        formStack.addArrangedSubview(
317
+        addFullWidthArrangedSubview(horizontalSeparator())
318
+        addFullWidthArrangedSubview(
284
             multilineProfileBlock(
319
             multilineProfileBlock(
285
                 title: "Interests",
320
                 title: "Interests",
286
                 placeholder: "List your interests and hobbies...",
321
                 placeholder: "List your interests and hobbies...",
@@ -288,8 +323,8 @@ final class MyProfilePageView: NSView {
288
                 minHeight: 100
323
                 minHeight: 100
289
             )
324
             )
290
         )
325
         )
291
-        formStack.addArrangedSubview(horizontalSeparator())
292
-        formStack.addArrangedSubview(
326
+        addFullWidthArrangedSubview(horizontalSeparator())
327
+        addFullWidthArrangedSubview(
293
             multilineProfileBlock(
328
             multilineProfileBlock(
294
                 title: "Languages",
329
                 title: "Languages",
295
                 placeholder: "List languages you speak (e.g., English - Native, Spanish - Fluent)...",
330
                 placeholder: "List languages you speak (e.g., English - Native, Spanish - Fluent)...",
@@ -297,9 +332,9 @@ final class MyProfilePageView: NSView {
297
                 minHeight: 100
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
         saveButton.target = self
339
         saveButton.target = self
305
         saveButton.action = #selector(didTapSave)
340
         saveButton.action = #selector(didTapSave)
@@ -315,14 +350,8 @@ final class MyProfilePageView: NSView {
315
         let compact = formWidth < Self.compactFormWidth
350
         let compact = formWidth < Self.compactFormWidth
316
         guard compact != lastCompactLayout else { return }
351
         guard compact != lastCompactLayout else { return }
317
         lastCompactLayout = compact
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
         for entry in workExperienceEntries {
355
         for entry in workExperienceEntries {
327
             entry.applyCompactLayout(compact)
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
     private func sectionHeading(_ text: String) -> NSView {
369
     private func sectionHeading(_ text: String) -> NSView {
@@ -374,6 +387,9 @@ final class MyProfilePageView: NSView {
374
         row.userInterfaceLayoutDirection = .leftToRight
387
         row.userInterfaceLayoutDirection = .leftToRight
375
         ProfileLayoutEnforcement.applyForcedLTR(to: row)
388
         ProfileLayoutEnforcement.applyForcedLTR(to: row)
376
         row.translatesAutoresizingMaskIntoConstraints = false
389
         row.translatesAutoresizingMaskIntoConstraints = false
390
+        NSLayoutConstraint.activate([
391
+            label.leftAnchor.constraint(equalTo: row.leftAnchor)
392
+        ])
377
         return row
393
         return row
378
     }
394
     }
379
 
395
 
@@ -400,7 +416,8 @@ final class MyProfilePageView: NSView {
400
         wrap.setContentHuggingPriority(.defaultLow, for: .horizontal)
416
         wrap.setContentHuggingPriority(.defaultLow, for: .horizontal)
401
         wrap.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
417
         wrap.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
402
         NSLayoutConstraint.activate([
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
         return stack
422
         return stack
406
     }
423
     }
@@ -512,6 +529,9 @@ final class MyProfilePageView: NSView {
512
         ProfileLayoutEnforcement.applyForcedLTR(to: stack)
529
         ProfileLayoutEnforcement.applyForcedLTR(to: stack)
513
         stack.setContentHuggingPriority(.defaultLow, for: .horizontal)
530
         stack.setContentHuggingPriority(.defaultLow, for: .horizontal)
514
         wrap.setContentHuggingPriority(.defaultLow, for: .horizontal)
531
         wrap.setContentHuggingPriority(.defaultLow, for: .horizontal)
532
+        NSLayoutConstraint.activate([
533
+            label.leftAnchor.constraint(equalTo: stack.leftAnchor)
534
+        ])
515
         return stack
535
         return stack
516
     }
536
     }
517
 
537
 
@@ -576,6 +596,9 @@ final class MyProfilePageView: NSView {
576
         ProfileLayoutEnforcement.applyForcedLTR(to: stack)
596
         ProfileLayoutEnforcement.applyForcedLTR(to: stack)
577
         stack.setContentHuggingPriority(.defaultLow, for: .horizontal)
597
         stack.setContentHuggingPriority(.defaultLow, for: .horizontal)
578
         wrap.setContentHuggingPriority(.defaultLow, for: .horizontal)
598
         wrap.setContentHuggingPriority(.defaultLow, for: .horizontal)
599
+        NSLayoutConstraint.activate([
600
+            label.leftAnchor.constraint(equalTo: stack.leftAnchor)
601
+        ])
579
         return stack
602
         return stack
580
     }
603
     }
581
 
604
 
@@ -608,6 +631,10 @@ final class MyProfilePageView: NSView {
608
         if #available(macOS 10.11, *) {
631
         if #available(macOS 10.11, *) {
609
             stack.setCustomSpacing(6, after: wrap)
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
         return stack
638
         return stack
612
     }
639
     }
613
 
640
 
@@ -826,7 +853,7 @@ private final class WorkExperienceEntryView: NSView {
826
     private let companyField = NSTextField()
853
     private let companyField = NSTextField()
827
     private let durationField = NSTextField()
854
     private let durationField = NSTextField()
828
     private let descriptionField = NSTextField()
855
     private let descriptionField = NSTextField()
829
-    private let jobCompanyRow = NSStackView()
856
+    private var jobCompanyRow: ProfileDualFieldRow!
830
 
857
 
831
     override init(frame frameRect: NSRect) {
858
     override init(frame frameRect: NSRect) {
832
         super.init(frame: frameRect)
859
         super.init(frame: frameRect)
@@ -846,9 +873,7 @@ private final class WorkExperienceEntryView: NSView {
846
     }
873
     }
847
 
874
 
848
     func applyCompactLayout(_ compact: Bool) {
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
     private func configure() {
879
     private func configure() {
@@ -902,8 +927,7 @@ private final class WorkExperienceEntryView: NSView {
902
 
927
 
903
         let jobGroup = Self.labeledFieldStack(title: "Job Title *", field: jobTitleField, placeholder: "e.g., Software Engineer")
928
         let jobGroup = Self.labeledFieldStack(title: "Job Title *", field: jobTitleField, placeholder: "e.g., Software Engineer")
904
         let companyGroup = Self.labeledFieldStack(title: "Company Name *", field: companyField, placeholder: "e.g., Google")
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
         let durationGroup = Self.labeledFieldStack(title: "Duration *", field: durationField, placeholder: "e.g., Jan 2020 - Present")
932
         let durationGroup = Self.labeledFieldStack(title: "Duration *", field: durationField, placeholder: "e.g., Jan 2020 - Present")
909
         let descriptionGroup = Self.multilineLabeledStack(
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
     override func layout() {
958
     override func layout() {
949
         super.layout()
959
         super.layout()
950
         for field in [jobTitleField, companyField, durationField, descriptionField] {
960
         for field in [jobTitleField, companyField, durationField, descriptionField] {
@@ -960,13 +970,6 @@ private final class WorkExperienceEntryView: NSView {
960
         layer.shadowPath = CGPath(roundedRect: bounds, cornerWidth: r, cornerHeight: r, transform: nil)
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
     @objc private func didTapDelete() {
973
     @objc private func didTapDelete() {
971
         onDelete?()
974
         onDelete?()
972
     }
975
     }
@@ -988,7 +991,13 @@ private final class WorkExperienceEntryView: NSView {
988
         stack.translatesAutoresizingMaskIntoConstraints = false
991
         stack.translatesAutoresizingMaskIntoConstraints = false
989
         stack.userInterfaceLayoutDirection = .leftToRight
992
         stack.userInterfaceLayoutDirection = .leftToRight
990
         ProfileLayoutEnforcement.applyForcedLTR(to: stack)
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
         return stack
1001
         return stack
993
     }
1002
     }
994
 
1003
 
@@ -1048,6 +1057,11 @@ private final class WorkExperienceEntryView: NSView {
1048
         stack.translatesAutoresizingMaskIntoConstraints = false
1057
         stack.translatesAutoresizingMaskIntoConstraints = false
1049
         stack.userInterfaceLayoutDirection = .leftToRight
1058
         stack.userInterfaceLayoutDirection = .leftToRight
1050
         ProfileLayoutEnforcement.applyForcedLTR(to: stack)
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
         return stack
1065
         return stack
1052
     }
1066
     }
1053
 
1067
 
@@ -1103,7 +1117,7 @@ private final class EducationEntryView: NSView {
1103
     private let degreeField = NSTextField()
1117
     private let degreeField = NSTextField()
1104
     private let institutionField = NSTextField()
1118
     private let institutionField = NSTextField()
1105
     private let yearField = NSTextField()
1119
     private let yearField = NSTextField()
1106
-    private let degreeInstitutionRow = NSStackView()
1120
+    private var degreeInstitutionRow: ProfileDualFieldRow!
1107
 
1121
 
1108
     override init(frame frameRect: NSRect) {
1122
     override init(frame frameRect: NSRect) {
1109
         super.init(frame: frameRect)
1123
         super.init(frame: frameRect)
@@ -1123,9 +1137,7 @@ private final class EducationEntryView: NSView {
1123
     }
1137
     }
1124
 
1138
 
1125
     func applyCompactLayout(_ compact: Bool) {
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
     private func configure() {
1143
     private func configure() {
@@ -1187,8 +1199,7 @@ private final class EducationEntryView: NSView {
1187
             field: institutionField,
1199
             field: institutionField,
1188
             placeholder: "e.g., MIT"
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
         let yearGroup = WorkExperienceEntryView.labeledFieldStack(
1204
         let yearGroup = WorkExperienceEntryView.labeledFieldStack(
1194
             title: "Year *",
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
     override func layout() {
1228
     override func layout() {
1232
         super.layout()
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
         guard let layer = layer, layer.shadowOpacity > 0 else { return }
1238
         guard let layer = layer, layer.shadowOpacity > 0 else { return }
1234
         let r = layer.cornerRadius
1239
         let r = layer.cornerRadius
1235
         layer.shadowPath = CGPath(roundedRect: bounds, cornerWidth: r, cornerHeight: r, transform: nil)
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
     @objc private func didTapDelete() {
1243
     @objc private func didTapDelete() {
1246
         onDelete?()
1244
         onDelete?()
1247
     }
1245
     }