|
|
@@ -22,6 +22,14 @@ private enum ProfilePagePalette {
|
|
|
|
|
|
/// Keeps profile text left-aligned and LTR so fields do not collapse to a narrow trailing strip under RTL / natural alignment.
|
|
|
private enum ProfileLayoutEnforcement {
|
|
|
+ static func applyForcedLTRSubtree(from root: NSView) {
|
|
|
+ var stack: [NSView] = [root]
|
|
|
+ while let view = stack.popLast() {
|
|
|
+ applyForcedLTR(to: view)
|
|
|
+ stack.append(contentsOf: view.subviews)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
static func applyForcedLTR(to view: NSView) {
|
|
|
view.userInterfaceLayoutDirection = .leftToRight
|
|
|
}
|
|
|
@@ -44,6 +52,15 @@ private enum ProfileLayoutEnforcement {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+private extension NSStackView {
|
|
|
+ /// For vertical stacks using `.leading` alignment (geometric left under mixed RTL), pin each arranged subview’s width to the stack so labels/fields stay full-width.
|
|
|
+ func pinAllArrangedSubviewWidthsEqualToStackWidth() {
|
|
|
+ for subview in arrangedSubviews {
|
|
|
+ subview.widthAnchor.constraint(equalTo: widthAnchor).isActive = true
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
/// 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
|
|
|
@@ -158,6 +175,13 @@ final class MyProfilePageView: NSView {
|
|
|
setup()
|
|
|
}
|
|
|
|
|
|
+ override func viewDidMoveToWindow() {
|
|
|
+ super.viewDidMoveToWindow()
|
|
|
+ guard window != nil else { return }
|
|
|
+ ProfileLayoutEnforcement.applyForcedLTRSubtree(from: self)
|
|
|
+ needsLayout = true
|
|
|
+ }
|
|
|
+
|
|
|
override func layout() {
|
|
|
super.layout()
|
|
|
if let layer = cardView.layer, layer.shadowOpacity > 0 {
|
|
|
@@ -243,7 +267,7 @@ final class MyProfilePageView: NSView {
|
|
|
|
|
|
formStack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
formStack.orientation = .vertical
|
|
|
- formStack.alignment = .width
|
|
|
+ formStack.alignment = .leading
|
|
|
formStack.distribution = .fill
|
|
|
formStack.spacing = 24
|
|
|
formStack.edgeInsets = NSEdgeInsets(top: 32, left: 28, bottom: 32, right: 28)
|
|
|
@@ -266,7 +290,6 @@ final class MyProfilePageView: NSView {
|
|
|
// Pin the document to the clip view’s geometric width so LTR/RTL semantics cannot slide the form.
|
|
|
documentView.leftAnchor.constraint(equalTo: scrollView.contentView.leftAnchor),
|
|
|
documentView.rightAnchor.constraint(equalTo: scrollView.contentView.rightAnchor),
|
|
|
- documentView.widthAnchor.constraint(equalTo: scrollView.contentView.widthAnchor),
|
|
|
documentView.topAnchor.constraint(equalTo: scrollView.contentView.topAnchor),
|
|
|
documentView.bottomAnchor.constraint(equalTo: cardView.bottomAnchor, constant: Self.horizontalPageInset),
|
|
|
|
|
|
@@ -341,6 +364,8 @@ final class MyProfilePageView: NSView {
|
|
|
|
|
|
appendWorkExperienceEntry()
|
|
|
appendEducationEntry()
|
|
|
+
|
|
|
+ ProfileLayoutEnforcement.applyForcedLTRSubtree(from: self)
|
|
|
}
|
|
|
|
|
|
private func applyResponsiveRowsIfNeeded() {
|
|
|
@@ -364,6 +389,7 @@ final class MyProfilePageView: NSView {
|
|
|
formStack.addArrangedSubview(view)
|
|
|
view.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
|
+ view.widthAnchor.constraint(equalTo: formStack.widthAnchor).isActive = true
|
|
|
}
|
|
|
|
|
|
private func sectionHeading(_ text: String) -> NSView {
|
|
|
@@ -398,7 +424,7 @@ final class MyProfilePageView: NSView {
|
|
|
label.font = .systemFont(ofSize: 12, weight: .medium)
|
|
|
label.textColor = ProfilePagePalette.secondaryText
|
|
|
label.translatesAutoresizingMaskIntoConstraints = false
|
|
|
- label.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
|
|
+ label.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
ProfileLayoutEnforcement.applyLeftAlignedTextField(label)
|
|
|
|
|
|
styleSingleLineField(field, placeholder: placeholder)
|
|
|
@@ -407,8 +433,8 @@ final class MyProfilePageView: NSView {
|
|
|
let stack = NSStackView(views: [label, wrap])
|
|
|
stack.orientation = .vertical
|
|
|
stack.spacing = 8
|
|
|
- // `.width` stretches label + field chrome to the row width; `.leading` collapses to intrinsic width and caused right-edge compression.
|
|
|
- stack.alignment = .width
|
|
|
+ // `.leading` keeps rows on the geometric left; explicit widths keep labels/fields full-width (`.width` alone can still hug the trailing edge under RTL-style layout).
|
|
|
+ stack.alignment = .leading
|
|
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
stack.userInterfaceLayoutDirection = .leftToRight
|
|
|
ProfileLayoutEnforcement.applyForcedLTR(to: stack)
|
|
|
@@ -416,8 +442,9 @@ final class MyProfilePageView: NSView {
|
|
|
wrap.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
wrap.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
|
NSLayoutConstraint.activate([
|
|
|
- wrap.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
|
|
- label.leftAnchor.constraint(equalTo: stack.leftAnchor)
|
|
|
+ label.leftAnchor.constraint(equalTo: stack.leftAnchor),
|
|
|
+ label.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
|
|
+ wrap.widthAnchor.constraint(equalTo: stack.widthAnchor)
|
|
|
])
|
|
|
return stack
|
|
|
}
|
|
|
@@ -523,14 +550,16 @@ final class MyProfilePageView: NSView {
|
|
|
let stack = NSStackView(views: [label, wrap])
|
|
|
stack.orientation = .vertical
|
|
|
stack.spacing = 8
|
|
|
- stack.alignment = .width
|
|
|
+ stack.alignment = .leading
|
|
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
stack.userInterfaceLayoutDirection = .leftToRight
|
|
|
ProfileLayoutEnforcement.applyForcedLTR(to: stack)
|
|
|
stack.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
wrap.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
NSLayoutConstraint.activate([
|
|
|
- label.leftAnchor.constraint(equalTo: stack.leftAnchor)
|
|
|
+ label.leftAnchor.constraint(equalTo: stack.leftAnchor),
|
|
|
+ label.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
|
|
+ wrap.widthAnchor.constraint(equalTo: stack.widthAnchor)
|
|
|
])
|
|
|
return stack
|
|
|
}
|
|
|
@@ -590,14 +619,16 @@ final class MyProfilePageView: NSView {
|
|
|
let stack = NSStackView(views: [label, wrap])
|
|
|
stack.orientation = .vertical
|
|
|
stack.spacing = 8
|
|
|
- stack.alignment = .width
|
|
|
+ stack.alignment = .leading
|
|
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
stack.userInterfaceLayoutDirection = .leftToRight
|
|
|
ProfileLayoutEnforcement.applyForcedLTR(to: stack)
|
|
|
stack.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
wrap.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
NSLayoutConstraint.activate([
|
|
|
- label.leftAnchor.constraint(equalTo: stack.leftAnchor)
|
|
|
+ label.leftAnchor.constraint(equalTo: stack.leftAnchor),
|
|
|
+ label.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
|
|
+ wrap.widthAnchor.constraint(equalTo: stack.widthAnchor)
|
|
|
])
|
|
|
return stack
|
|
|
}
|
|
|
@@ -622,7 +653,7 @@ final class MyProfilePageView: NSView {
|
|
|
let stack = NSStackView(views: [label, wrap, helper])
|
|
|
stack.orientation = .vertical
|
|
|
stack.spacing = 8
|
|
|
- stack.alignment = .width
|
|
|
+ stack.alignment = .leading
|
|
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
stack.userInterfaceLayoutDirection = .leftToRight
|
|
|
ProfileLayoutEnforcement.applyForcedLTR(to: stack)
|
|
|
@@ -633,7 +664,10 @@ final class MyProfilePageView: NSView {
|
|
|
}
|
|
|
NSLayoutConstraint.activate([
|
|
|
label.leftAnchor.constraint(equalTo: stack.leftAnchor),
|
|
|
- helper.leftAnchor.constraint(equalTo: stack.leftAnchor)
|
|
|
+ label.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
|
|
+ wrap.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
|
|
+ helper.leftAnchor.constraint(equalTo: stack.leftAnchor),
|
|
|
+ helper.widthAnchor.constraint(equalTo: stack.widthAnchor)
|
|
|
])
|
|
|
return stack
|
|
|
}
|
|
|
@@ -677,17 +711,18 @@ final class MyProfilePageView: NSView {
|
|
|
workExperienceRowsStack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
workExperienceRowsStack.orientation = .vertical
|
|
|
workExperienceRowsStack.spacing = 20
|
|
|
- workExperienceRowsStack.alignment = .width
|
|
|
+ workExperienceRowsStack.alignment = .leading
|
|
|
workExperienceRowsStack.userInterfaceLayoutDirection = .leftToRight
|
|
|
ProfileLayoutEnforcement.applyForcedLTR(to: workExperienceRowsStack)
|
|
|
|
|
|
let outer = NSStackView(views: [headerRow, workExperienceRowsStack])
|
|
|
outer.orientation = .vertical
|
|
|
outer.spacing = 16
|
|
|
- outer.alignment = .width
|
|
|
+ outer.alignment = .leading
|
|
|
outer.translatesAutoresizingMaskIntoConstraints = false
|
|
|
outer.userInterfaceLayoutDirection = .leftToRight
|
|
|
ProfileLayoutEnforcement.applyForcedLTR(to: outer)
|
|
|
+ outer.pinAllArrangedSubviewWidthsEqualToStackWidth()
|
|
|
return outer
|
|
|
}
|
|
|
|
|
|
@@ -722,17 +757,18 @@ final class MyProfilePageView: NSView {
|
|
|
educationRowsStack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
educationRowsStack.orientation = .vertical
|
|
|
educationRowsStack.spacing = 16
|
|
|
- educationRowsStack.alignment = .width
|
|
|
+ educationRowsStack.alignment = .leading
|
|
|
educationRowsStack.userInterfaceLayoutDirection = .leftToRight
|
|
|
ProfileLayoutEnforcement.applyForcedLTR(to: educationRowsStack)
|
|
|
|
|
|
let outer = NSStackView(views: [headerRow, educationRowsStack])
|
|
|
outer.orientation = .vertical
|
|
|
outer.spacing = 16
|
|
|
- outer.alignment = .width
|
|
|
+ outer.alignment = .leading
|
|
|
outer.translatesAutoresizingMaskIntoConstraints = false
|
|
|
outer.userInterfaceLayoutDirection = .leftToRight
|
|
|
ProfileLayoutEnforcement.applyForcedLTR(to: outer)
|
|
|
+ outer.pinAllArrangedSubviewWidthsEqualToStackWidth()
|
|
|
return outer
|
|
|
}
|
|
|
|
|
|
@@ -748,6 +784,7 @@ final class MyProfilePageView: NSView {
|
|
|
}
|
|
|
workExperienceEntries.append(entry)
|
|
|
workExperienceRowsStack.addArrangedSubview(entry)
|
|
|
+ entry.widthAnchor.constraint(equalTo: workExperienceRowsStack.widthAnchor).isActive = true
|
|
|
renumberWorkExperienceEntries()
|
|
|
refreshWorkExperienceDeleteButtons()
|
|
|
}
|
|
|
@@ -786,6 +823,7 @@ final class MyProfilePageView: NSView {
|
|
|
}
|
|
|
educationEntries.append(entry)
|
|
|
educationRowsStack.addArrangedSubview(entry)
|
|
|
+ entry.widthAnchor.constraint(equalTo: educationRowsStack.widthAnchor).isActive = true
|
|
|
renumberEducationEntries()
|
|
|
refreshEducationDeleteButtons()
|
|
|
}
|
|
|
@@ -940,11 +978,12 @@ private final class WorkExperienceEntryView: NSView {
|
|
|
let inner = NSStackView(views: [headerRow, jobCompanyRow, durationGroup, descriptionGroup])
|
|
|
inner.orientation = .vertical
|
|
|
inner.spacing = 16
|
|
|
- inner.alignment = .width
|
|
|
+ inner.alignment = .leading
|
|
|
inner.translatesAutoresizingMaskIntoConstraints = false
|
|
|
inner.edgeInsets = NSEdgeInsets(top: 16, left: 18, bottom: 16, right: 18)
|
|
|
inner.userInterfaceLayoutDirection = .leftToRight
|
|
|
ProfileLayoutEnforcement.applyForcedLTR(to: inner)
|
|
|
+ inner.pinAllArrangedSubviewWidthsEqualToStackWidth()
|
|
|
|
|
|
addSubview(inner)
|
|
|
NSLayoutConstraint.activate([
|
|
|
@@ -987,7 +1026,7 @@ private final class WorkExperienceEntryView: NSView {
|
|
|
let stack = NSStackView(views: [label, wrap])
|
|
|
stack.orientation = .vertical
|
|
|
stack.spacing = 8
|
|
|
- stack.alignment = .width
|
|
|
+ stack.alignment = .leading
|
|
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
stack.userInterfaceLayoutDirection = .leftToRight
|
|
|
ProfileLayoutEnforcement.applyForcedLTR(to: stack)
|
|
|
@@ -995,8 +1034,9 @@ private final class WorkExperienceEntryView: NSView {
|
|
|
wrap.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
wrap.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
|
NSLayoutConstraint.activate([
|
|
|
- wrap.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
|
|
- label.leftAnchor.constraint(equalTo: stack.leftAnchor)
|
|
|
+ label.leftAnchor.constraint(equalTo: stack.leftAnchor),
|
|
|
+ label.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
|
|
+ wrap.widthAnchor.constraint(equalTo: stack.widthAnchor)
|
|
|
])
|
|
|
return stack
|
|
|
}
|
|
|
@@ -1053,14 +1093,16 @@ private final class WorkExperienceEntryView: NSView {
|
|
|
let stack = NSStackView(views: [label, wrap])
|
|
|
stack.orientation = .vertical
|
|
|
stack.spacing = 8
|
|
|
- stack.alignment = .width
|
|
|
+ stack.alignment = .leading
|
|
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
stack.userInterfaceLayoutDirection = .leftToRight
|
|
|
ProfileLayoutEnforcement.applyForcedLTR(to: stack)
|
|
|
stack.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
wrap.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
NSLayoutConstraint.activate([
|
|
|
- label.leftAnchor.constraint(equalTo: stack.leftAnchor)
|
|
|
+ label.leftAnchor.constraint(equalTo: stack.leftAnchor),
|
|
|
+ label.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
|
|
+ wrap.widthAnchor.constraint(equalTo: stack.widthAnchor)
|
|
|
])
|
|
|
return stack
|
|
|
}
|
|
|
@@ -1210,11 +1252,12 @@ private final class EducationEntryView: NSView {
|
|
|
let inner = NSStackView(views: [headerRow, degreeInstitutionRow, yearGroup])
|
|
|
inner.orientation = .vertical
|
|
|
inner.spacing = 14
|
|
|
- inner.alignment = .width
|
|
|
+ inner.alignment = .leading
|
|
|
inner.translatesAutoresizingMaskIntoConstraints = false
|
|
|
inner.edgeInsets = NSEdgeInsets(top: 14, left: 18, bottom: 14, right: 18)
|
|
|
inner.userInterfaceLayoutDirection = .leftToRight
|
|
|
ProfileLayoutEnforcement.applyForcedLTR(to: inner)
|
|
|
+ inner.pinAllArrangedSubviewWidthsEqualToStackWidth()
|
|
|
|
|
|
addSubview(inner)
|
|
|
NSLayoutConstraint.activate([
|