|
@@ -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?()
|
|
|
}
|
|
}
|