|
@@ -17,14 +17,43 @@ private enum ProfilePagePalette {
|
|
|
static let primaryText = NSColor(srgbRed: 45 / 255, green: 45 / 255, blue: 45 / 255, alpha: 1)
|
|
static let primaryText = NSColor(srgbRed: 45 / 255, green: 45 / 255, blue: 45 / 255, alpha: 1)
|
|
|
static let secondaryText = NSColor(srgbRed: 118 / 255, green: 118 / 255, blue: 118 / 255, alpha: 1)
|
|
static let secondaryText = NSColor(srgbRed: 118 / 255, green: 118 / 255, blue: 118 / 255, alpha: 1)
|
|
|
static let border = NSColor(srgbRed: 212 / 255, green: 210 / 255, blue: 208 / 255, alpha: 1)
|
|
static let border = NSColor(srgbRed: 212 / 255, green: 210 / 255, blue: 208 / 255, alpha: 1)
|
|
|
|
|
+ static let destructive = NSColor(srgbRed: 220 / 255, green: 38 / 255, blue: 38 / 255, alpha: 1)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/// 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 applyForcedLTR(to view: NSView) {
|
|
|
|
|
+ view.userInterfaceLayoutDirection = .leftToRight
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ static func applyLeftAlignedTextField(_ field: NSTextField) {
|
|
|
|
|
+ applyForcedLTR(to: field)
|
|
|
|
|
+ field.baseWritingDirection = .leftToRight
|
|
|
|
|
+ field.alignment = .left
|
|
|
|
|
+ if let cell = field.cell as? NSTextFieldCell {
|
|
|
|
|
+ cell.alignment = .left
|
|
|
|
|
+ cell.baseWritingDirection = .leftToRight
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ static func leftAlignedParagraphStyle() -> NSParagraphStyle {
|
|
|
|
|
+ let p = NSMutableParagraphStyle()
|
|
|
|
|
+ p.alignment = .left
|
|
|
|
|
+ p.baseWritingDirection = .leftToRight
|
|
|
|
|
+ return p
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
final class MyProfilePageView: NSView {
|
|
final class MyProfilePageView: NSView {
|
|
|
- private static let compactFormWidth: CGFloat = 520
|
|
|
|
|
|
|
+ /// Below this form content width, two-column rows stack vertically.
|
|
|
|
|
+ private static let compactFormWidth: CGFloat = 640
|
|
|
|
|
+ private static let readableFormMaxWidth: CGFloat = 880
|
|
|
|
|
+ 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()
|
|
@@ -34,13 +63,24 @@ final class MyProfilePageView: NSView {
|
|
|
private let jobTitleField = NSTextField()
|
|
private let jobTitleField = NSTextField()
|
|
|
private let addressField = NSTextField()
|
|
private let addressField = NSTextField()
|
|
|
private let careerField = NSTextField()
|
|
private let careerField = NSTextField()
|
|
|
|
|
+ private let certificatesField = NSTextField()
|
|
|
|
|
+ private let interestsField = NSTextField()
|
|
|
|
|
+ private let languagesField = 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 nameEmailRow = NSStackView()
|
|
|
private let phoneJobRow = NSStackView()
|
|
private let phoneJobRow = NSStackView()
|
|
|
|
|
|
|
|
|
|
+ private let workExperienceRowsStack = NSStackView()
|
|
|
|
|
+ private var workExperienceEntries: [WorkExperienceEntryView] = []
|
|
|
|
|
+ private let educationRowsStack = NSStackView()
|
|
|
|
|
+ private var educationEntries: [EducationEntryView] = []
|
|
|
|
|
+
|
|
|
private var lastCompactLayout: Bool?
|
|
private var lastCompactLayout: Bool?
|
|
|
|
|
|
|
|
|
|
+ private var referralHelperLabel: NSTextField?
|
|
|
|
|
+
|
|
|
/// Force left-to-right geometry so profile fields span the full width even when the window uses RTL layout.
|
|
/// Force left-to-right geometry so profile fields span the full width even when the window uses RTL layout.
|
|
|
override var userInterfaceLayoutDirection: NSUserInterfaceLayoutDirection {
|
|
override var userInterfaceLayoutDirection: NSUserInterfaceLayoutDirection {
|
|
|
get { .leftToRight }
|
|
get { .leftToRight }
|
|
@@ -57,18 +97,77 @@ 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 {
|
|
|
|
|
+ let r = layer.cornerRadius
|
|
|
|
|
+ layer.shadowPath = CGPath(roundedRect: cardView.bounds, cornerWidth: r, cornerHeight: r, transform: nil)
|
|
|
|
|
+ }
|
|
|
|
|
+ updateMultilinePreferredLayoutWidths()
|
|
|
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.
|
|
|
|
|
+ private func updateMultilinePreferredLayoutWidths() {
|
|
|
|
|
+ let horizontalInset: CGFloat = 24
|
|
|
|
|
+ applyPreferredWrapWidth(to: profileNameField, horizontalInset: horizontalInset)
|
|
|
|
|
+ applyPreferredWrapWidth(to: fullNameField, horizontalInset: horizontalInset)
|
|
|
|
|
+ applyPreferredWrapWidth(to: emailField, horizontalInset: horizontalInset)
|
|
|
|
|
+ applyPreferredWrapWidth(to: phoneField, horizontalInset: horizontalInset)
|
|
|
|
|
+ applyPreferredWrapWidth(to: jobTitleField, horizontalInset: horizontalInset)
|
|
|
|
|
+ applyPreferredWrapWidth(to: addressField, horizontalInset: horizontalInset)
|
|
|
|
|
+ applyPreferredWrapWidth(to: referralField, horizontalInset: horizontalInset)
|
|
|
|
|
+ applyPreferredWrapWidth(to: careerField, horizontalInset: horizontalInset)
|
|
|
|
|
+ applyPreferredWrapWidth(to: certificatesField, horizontalInset: horizontalInset)
|
|
|
|
|
+ applyPreferredWrapWidth(to: interestsField, horizontalInset: horizontalInset)
|
|
|
|
|
+ applyPreferredWrapWidth(to: languagesField, horizontalInset: horizontalInset)
|
|
|
|
|
+ if let helper = referralHelperLabel, let stack = helper.superview, stack.bounds.width > 2 {
|
|
|
|
|
+ let w = max(1, stack.bounds.width - 8)
|
|
|
|
|
+ if abs(helper.preferredMaxLayoutWidth - w) > 0.5 {
|
|
|
|
|
+ helper.preferredMaxLayoutWidth = w
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func applyPreferredWrapWidth(to field: NSTextField, horizontalInset: CGFloat) {
|
|
|
|
|
+ guard let wrap = field.superview, wrap.bounds.width > 2 else { return }
|
|
|
|
|
+ let w = max(1, wrap.bounds.width - horizontalInset)
|
|
|
|
|
+ if abs(field.preferredMaxLayoutWidth - w) > 0.5 {
|
|
|
|
|
+ field.preferredMaxLayoutWidth = w
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private func setup() {
|
|
private func setup() {
|
|
|
wantsLayer = true
|
|
wantsLayer = true
|
|
|
layer?.backgroundColor = ProfilePagePalette.pageBackground.cgColor
|
|
layer?.backgroundColor = ProfilePagePalette.pageBackground.cgColor
|
|
|
userInterfaceLayoutDirection = .leftToRight
|
|
userInterfaceLayoutDirection = .leftToRight
|
|
|
|
|
+ ProfileLayoutEnforcement.applyForcedLTR(to: self)
|
|
|
|
|
|
|
|
scrollView.translatesAutoresizingMaskIntoConstraints = false
|
|
scrollView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
scrollView.userInterfaceLayoutDirection = .leftToRight
|
|
scrollView.userInterfaceLayoutDirection = .leftToRight
|
|
|
|
|
+ ProfileLayoutEnforcement.applyForcedLTR(to: scrollView)
|
|
|
scrollView.hasVerticalScroller = true
|
|
scrollView.hasVerticalScroller = true
|
|
|
scrollView.hasHorizontalScroller = false
|
|
scrollView.hasHorizontalScroller = false
|
|
|
scrollView.autohidesScrollers = true
|
|
scrollView.autohidesScrollers = true
|
|
@@ -77,28 +176,43 @@ final class MyProfilePageView: NSView {
|
|
|
scrollView.scrollerStyle = .overlay
|
|
scrollView.scrollerStyle = .overlay
|
|
|
scrollView.automaticallyAdjustsContentInsets = false
|
|
scrollView.automaticallyAdjustsContentInsets = false
|
|
|
scrollView.contentView.userInterfaceLayoutDirection = .leftToRight
|
|
scrollView.contentView.userInterfaceLayoutDirection = .leftToRight
|
|
|
|
|
+ ProfileLayoutEnforcement.applyForcedLTR(to: scrollView.contentView)
|
|
|
|
|
+ if #available(macOS 10.11, *) {
|
|
|
|
|
+ scrollView.horizontalScrollElasticity = .none
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
documentView.translatesAutoresizingMaskIntoConstraints = false
|
|
documentView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
documentView.userInterfaceLayoutDirection = .leftToRight
|
|
documentView.userInterfaceLayoutDirection = .leftToRight
|
|
|
|
|
+ ProfileLayoutEnforcement.applyForcedLTR(to: documentView)
|
|
|
|
|
|
|
|
cardView.translatesAutoresizingMaskIntoConstraints = false
|
|
cardView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
cardView.wantsLayer = true
|
|
cardView.wantsLayer = true
|
|
|
cardView.layer?.backgroundColor = ProfilePagePalette.cardBackground.cgColor
|
|
cardView.layer?.backgroundColor = ProfilePagePalette.cardBackground.cgColor
|
|
|
- cardView.layer?.cornerRadius = 16
|
|
|
|
|
|
|
+ cardView.layer?.cornerRadius = 18
|
|
|
cardView.layer?.borderWidth = 1
|
|
cardView.layer?.borderWidth = 1
|
|
|
cardView.layer?.borderColor = ProfilePagePalette.border.cgColor
|
|
cardView.layer?.borderColor = ProfilePagePalette.border.cgColor
|
|
|
|
|
+ cardView.layer?.masksToBounds = false
|
|
|
|
|
+ cardView.layer?.shadowColor = NSColor.black.cgColor
|
|
|
|
|
+ cardView.layer?.shadowOpacity = 0.06
|
|
|
|
|
+ cardView.layer?.shadowRadius = 20
|
|
|
|
|
+ cardView.layer?.shadowOffset = CGSize(width: 0, height: 10)
|
|
|
cardView.userInterfaceLayoutDirection = .leftToRight
|
|
cardView.userInterfaceLayoutDirection = .leftToRight
|
|
|
|
|
+ ProfileLayoutEnforcement.applyForcedLTR(to: cardView)
|
|
|
if #available(macOS 11.0, *) {
|
|
if #available(macOS 11.0, *) {
|
|
|
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
|
|
|
formStack.distribution = .fill
|
|
formStack.distribution = .fill
|
|
|
- formStack.spacing = 20
|
|
|
|
|
- formStack.edgeInsets = NSEdgeInsets(top: 28, left: 22, bottom: 28, right: 22)
|
|
|
|
|
|
|
+ formStack.spacing = 24
|
|
|
|
|
+ formStack.edgeInsets = NSEdgeInsets(top: 32, left: 28, bottom: 32, right: 28)
|
|
|
formStack.userInterfaceLayoutDirection = .leftToRight
|
|
formStack.userInterfaceLayoutDirection = .leftToRight
|
|
|
|
|
+ ProfileLayoutEnforcement.applyForcedLTR(to: formStack)
|
|
|
formStack.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
formStack.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
formStack.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
formStack.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
|
|
|
|
|
@@ -108,24 +222,26 @@ final class MyProfilePageView: NSView {
|
|
|
cardView.addSubview(formStack)
|
|
cardView.addSubview(formStack)
|
|
|
|
|
|
|
|
NSLayoutConstraint.activate([
|
|
NSLayoutConstraint.activate([
|
|
|
- scrollView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
|
|
|
- scrollView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
|
|
|
|
|
+ scrollView.leftAnchor.constraint(equalTo: leftAnchor),
|
|
|
|
|
+ scrollView.rightAnchor.constraint(equalTo: rightAnchor),
|
|
|
scrollView.topAnchor.constraint(equalTo: topAnchor),
|
|
scrollView.topAnchor.constraint(equalTo: topAnchor),
|
|
|
scrollView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
scrollView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
|
|
|
|
|
|
- // Pin left and right to the clip view’s geometric edges so the document spans the full visible
|
|
|
|
|
- // width (leading/trailing + width alone can leave a narrow strip on the wrong side in edge cases).
|
|
|
|
|
|
|
+ // 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.leftAnchor.constraint(equalTo: scrollView.contentView.leftAnchor),
|
|
|
documentView.rightAnchor.constraint(equalTo: scrollView.contentView.rightAnchor),
|
|
documentView.rightAnchor.constraint(equalTo: scrollView.contentView.rightAnchor),
|
|
|
|
|
+ documentView.widthAnchor.constraint(equalTo: scrollView.contentView.widthAnchor),
|
|
|
documentView.topAnchor.constraint(equalTo: scrollView.contentView.topAnchor),
|
|
documentView.topAnchor.constraint(equalTo: scrollView.contentView.topAnchor),
|
|
|
|
|
+ documentView.bottomAnchor.constraint(equalTo: cardView.bottomAnchor, constant: Self.horizontalPageInset),
|
|
|
|
|
|
|
|
- cardView.leadingAnchor.constraint(equalTo: documentView.leadingAnchor, constant: 20),
|
|
|
|
|
- cardView.trailingAnchor.constraint(equalTo: documentView.trailingAnchor, constant: -20),
|
|
|
|
|
- cardView.topAnchor.constraint(equalTo: documentView.topAnchor, constant: 16),
|
|
|
|
|
- cardView.bottomAnchor.constraint(equalTo: documentView.bottomAnchor, constant: -24),
|
|
|
|
|
|
|
+ cardView.leadingAnchor.constraint(equalTo: documentView.leadingAnchor, constant: Self.horizontalPageInset),
|
|
|
|
|
+ cardView.trailingAnchor.constraint(lessThanOrEqualTo: documentView.trailingAnchor, constant: -Self.horizontalPageInset),
|
|
|
|
|
+ cardView.topAnchor.constraint(equalTo: documentView.topAnchor, constant: Self.horizontalPageInset),
|
|
|
|
|
+ readableFormWidthConstraint,
|
|
|
|
|
|
|
|
formStack.leadingAnchor.constraint(equalTo: cardView.leadingAnchor),
|
|
formStack.leadingAnchor.constraint(equalTo: cardView.leadingAnchor),
|
|
|
formStack.trailingAnchor.constraint(equalTo: cardView.trailingAnchor),
|
|
formStack.trailingAnchor.constraint(equalTo: cardView.trailingAnchor),
|
|
|
|
|
+ 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)
|
|
|
])
|
|
])
|
|
@@ -145,12 +261,51 @@ final class MyProfilePageView: NSView {
|
|
|
pinEqualColumnWidths(in: phoneJobRow)
|
|
pinEqualColumnWidths(in: phoneJobRow)
|
|
|
formStack.addArrangedSubview(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(labeledGroup(title: "Address", field: addressField, placeholder: "123 Main St, City, State, ZIP"))
|
|
|
formStack.addArrangedSubview(careerSummaryBlock())
|
|
formStack.addArrangedSubview(careerSummaryBlock())
|
|
|
|
|
+ formStack.addArrangedSubview(horizontalSeparator())
|
|
|
|
|
+ formStack.addArrangedSubview(workExperienceSection())
|
|
|
|
|
+ formStack.addArrangedSubview(horizontalSeparator())
|
|
|
|
|
+ formStack.addArrangedSubview(educationSection())
|
|
|
|
|
+ formStack.addArrangedSubview(horizontalSeparator())
|
|
|
|
|
+ formStack.addArrangedSubview(
|
|
|
|
|
+ multilineProfileBlock(
|
|
|
|
|
+ title: "Certificates / Rewards",
|
|
|
|
|
+ placeholder: "List your certificates and awards...",
|
|
|
|
|
+ field: certificatesField,
|
|
|
|
|
+ minHeight: 100
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ formStack.addArrangedSubview(horizontalSeparator())
|
|
|
|
|
+ formStack.addArrangedSubview(
|
|
|
|
|
+ multilineProfileBlock(
|
|
|
|
|
+ title: "Interests",
|
|
|
|
|
+ placeholder: "List your interests and hobbies...",
|
|
|
|
|
+ field: interestsField,
|
|
|
|
|
+ minHeight: 100
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ formStack.addArrangedSubview(horizontalSeparator())
|
|
|
|
|
+ formStack.addArrangedSubview(
|
|
|
|
|
+ multilineProfileBlock(
|
|
|
|
|
+ title: "Languages",
|
|
|
|
|
+ placeholder: "List languages you speak (e.g., English - Native, Spanish - Fluent)...",
|
|
|
|
|
+ field: languagesField,
|
|
|
|
|
+ minHeight: 100
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ formStack.addArrangedSubview(horizontalSeparator())
|
|
|
|
|
+ formStack.addArrangedSubview(referralBlock())
|
|
|
formStack.addArrangedSubview(saveButtonHost())
|
|
formStack.addArrangedSubview(saveButtonHost())
|
|
|
|
|
|
|
|
saveButton.target = self
|
|
saveButton.target = self
|
|
|
saveButton.action = #selector(didTapSave)
|
|
saveButton.action = #selector(didTapSave)
|
|
|
|
|
+
|
|
|
|
|
+ appendWorkExperienceEntry()
|
|
|
|
|
+ appendEducationEntry()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private func applyResponsiveRowsIfNeeded() {
|
|
private func applyResponsiveRowsIfNeeded() {
|
|
@@ -168,6 +323,12 @@ final class MyProfilePageView: NSView {
|
|
|
phoneJobRow.spacing = rowSpacing
|
|
phoneJobRow.spacing = rowSpacing
|
|
|
nameEmailRow.distribution = compact ? .fill : .fillEqually
|
|
nameEmailRow.distribution = compact ? .fill : .fillEqually
|
|
|
phoneJobRow.distribution = compact ? .fill : .fillEqually
|
|
phoneJobRow.distribution = compact ? .fill : .fillEqually
|
|
|
|
|
+ for entry in workExperienceEntries {
|
|
|
|
|
+ entry.applyCompactLayout(compact)
|
|
|
|
|
+ }
|
|
|
|
|
+ for entry in educationEntries {
|
|
|
|
|
+ entry.applyCompactLayout(compact)
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// Keeps two columns the same width when the row is horizontal; avoids NSTextField intrinsic width fighting the stack.
|
|
/// Keeps two columns the same width when the row is horizontal; avoids NSTextField intrinsic width fighting the stack.
|
|
@@ -185,6 +346,7 @@ final class MyProfilePageView: NSView {
|
|
|
row.distribution = .fillEqually
|
|
row.distribution = .fillEqually
|
|
|
row.alignment = .top
|
|
row.alignment = .top
|
|
|
row.userInterfaceLayoutDirection = .leftToRight
|
|
row.userInterfaceLayoutDirection = .leftToRight
|
|
|
|
|
+ ProfileLayoutEnforcement.applyForcedLTR(to: row)
|
|
|
row.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
row.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
row.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
row.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
|
row.addArrangedSubview(left)
|
|
row.addArrangedSubview(left)
|
|
@@ -195,10 +357,10 @@ final class MyProfilePageView: NSView {
|
|
|
let label = NSTextField(labelWithString: text)
|
|
let label = NSTextField(labelWithString: text)
|
|
|
label.font = .systemFont(ofSize: 15, weight: .semibold)
|
|
label.font = .systemFont(ofSize: 15, weight: .semibold)
|
|
|
label.textColor = ProfilePagePalette.primaryText
|
|
label.textColor = ProfilePagePalette.primaryText
|
|
|
- label.alignment = .left
|
|
|
|
|
label.baseWritingDirection = .leftToRight
|
|
label.baseWritingDirection = .leftToRight
|
|
|
label.translatesAutoresizingMaskIntoConstraints = false
|
|
label.translatesAutoresizingMaskIntoConstraints = false
|
|
|
label.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
|
label.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
|
|
|
|
+ ProfileLayoutEnforcement.applyLeftAlignedTextField(label)
|
|
|
|
|
|
|
|
let spacer = NSView()
|
|
let spacer = NSView()
|
|
|
spacer.translatesAutoresizingMaskIntoConstraints = false
|
|
spacer.translatesAutoresizingMaskIntoConstraints = false
|
|
@@ -207,8 +369,10 @@ final class MyProfilePageView: NSView {
|
|
|
let row = NSStackView(views: [label, spacer])
|
|
let row = NSStackView(views: [label, spacer])
|
|
|
row.orientation = .horizontal
|
|
row.orientation = .horizontal
|
|
|
row.alignment = .centerY
|
|
row.alignment = .centerY
|
|
|
|
|
+ row.distribution = .fill
|
|
|
row.spacing = 0
|
|
row.spacing = 0
|
|
|
row.userInterfaceLayoutDirection = .leftToRight
|
|
row.userInterfaceLayoutDirection = .leftToRight
|
|
|
|
|
+ ProfileLayoutEnforcement.applyForcedLTR(to: row)
|
|
|
row.translatesAutoresizingMaskIntoConstraints = false
|
|
row.translatesAutoresizingMaskIntoConstraints = false
|
|
|
return row
|
|
return row
|
|
|
}
|
|
}
|
|
@@ -217,10 +381,9 @@ final class MyProfilePageView: NSView {
|
|
|
let label = NSTextField(labelWithString: title)
|
|
let label = NSTextField(labelWithString: title)
|
|
|
label.font = .systemFont(ofSize: 12, weight: .medium)
|
|
label.font = .systemFont(ofSize: 12, weight: .medium)
|
|
|
label.textColor = ProfilePagePalette.secondaryText
|
|
label.textColor = ProfilePagePalette.secondaryText
|
|
|
- label.alignment = .left
|
|
|
|
|
- label.baseWritingDirection = .leftToRight
|
|
|
|
|
label.translatesAutoresizingMaskIntoConstraints = false
|
|
label.translatesAutoresizingMaskIntoConstraints = false
|
|
|
label.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
|
label.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
|
|
|
|
+ ProfileLayoutEnforcement.applyLeftAlignedTextField(label)
|
|
|
|
|
|
|
|
styleSingleLineField(field, placeholder: placeholder)
|
|
styleSingleLineField(field, placeholder: placeholder)
|
|
|
let wrap = roundedFieldChrome(containing: field, minHeight: 40)
|
|
let wrap = roundedFieldChrome(containing: field, minHeight: 40)
|
|
@@ -228,10 +391,11 @@ final class MyProfilePageView: NSView {
|
|
|
let stack = NSStackView(views: [label, wrap])
|
|
let stack = NSStackView(views: [label, wrap])
|
|
|
stack.orientation = .vertical
|
|
stack.orientation = .vertical
|
|
|
stack.spacing = 8
|
|
stack.spacing = 8
|
|
|
- // Leading keeps the title at the left edge; width match keeps the field full width.
|
|
|
|
|
- stack.alignment = .leading
|
|
|
|
|
|
|
+ // `.width` stretches label + field chrome to the row width; `.leading` collapses to intrinsic width and caused right-edge compression.
|
|
|
|
|
+ stack.alignment = .width
|
|
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
stack.userInterfaceLayoutDirection = .leftToRight
|
|
stack.userInterfaceLayoutDirection = .leftToRight
|
|
|
|
|
+ ProfileLayoutEnforcement.applyForcedLTR(to: stack)
|
|
|
stack.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
stack.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
wrap.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
wrap.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
wrap.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
wrap.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
@@ -250,18 +414,19 @@ final class MyProfilePageView: NSView {
|
|
|
field.textColor = ProfilePagePalette.primaryText
|
|
field.textColor = ProfilePagePalette.primaryText
|
|
|
field.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
field.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
field.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
field.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
|
|
|
+ let paragraph = ProfileLayoutEnforcement.leftAlignedParagraphStyle()
|
|
|
field.placeholderAttributedString = NSAttributedString(
|
|
field.placeholderAttributedString = NSAttributedString(
|
|
|
string: placeholder,
|
|
string: placeholder,
|
|
|
attributes: [
|
|
attributes: [
|
|
|
.foregroundColor: ProfilePagePalette.secondaryText,
|
|
.foregroundColor: ProfilePagePalette.secondaryText,
|
|
|
- .font: NSFont.systemFont(ofSize: 14, weight: .regular)
|
|
|
|
|
|
|
+ .font: NSFont.systemFont(ofSize: 14, weight: .regular),
|
|
|
|
|
+ .paragraphStyle: paragraph
|
|
|
]
|
|
]
|
|
|
)
|
|
)
|
|
|
field.cell?.usesSingleLineMode = true
|
|
field.cell?.usesSingleLineMode = true
|
|
|
field.cell?.wraps = false
|
|
field.cell?.wraps = false
|
|
|
field.cell?.isScrollable = true
|
|
field.cell?.isScrollable = true
|
|
|
- field.baseWritingDirection = .leftToRight
|
|
|
|
|
- field.alignment = .left
|
|
|
|
|
|
|
+ ProfileLayoutEnforcement.applyLeftAlignedTextField(field)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private func roundedFieldChrome(containing field: NSTextField, minHeight: CGFloat) -> NSView {
|
|
private func roundedFieldChrome(containing field: NSTextField, minHeight: CGFloat) -> NSView {
|
|
@@ -276,6 +441,7 @@ final class MyProfilePageView: NSView {
|
|
|
wrap.layer?.cornerCurve = .continuous
|
|
wrap.layer?.cornerCurve = .continuous
|
|
|
}
|
|
}
|
|
|
wrap.addSubview(field)
|
|
wrap.addSubview(field)
|
|
|
|
|
+ ProfileLayoutEnforcement.applyForcedLTR(to: wrap)
|
|
|
NSLayoutConstraint.activate([
|
|
NSLayoutConstraint.activate([
|
|
|
field.leftAnchor.constraint(equalTo: wrap.leftAnchor, constant: 12),
|
|
field.leftAnchor.constraint(equalTo: wrap.leftAnchor, constant: 12),
|
|
|
field.rightAnchor.constraint(equalTo: wrap.rightAnchor, constant: -12),
|
|
field.rightAnchor.constraint(equalTo: wrap.rightAnchor, constant: -12),
|
|
@@ -289,8 +455,8 @@ final class MyProfilePageView: NSView {
|
|
|
let label = NSTextField(labelWithString: "Career Summary")
|
|
let label = NSTextField(labelWithString: "Career Summary")
|
|
|
label.font = .systemFont(ofSize: 12, weight: .medium)
|
|
label.font = .systemFont(ofSize: 12, weight: .medium)
|
|
|
label.textColor = ProfilePagePalette.secondaryText
|
|
label.textColor = ProfilePagePalette.secondaryText
|
|
|
- label.alignment = .left
|
|
|
|
|
label.translatesAutoresizingMaskIntoConstraints = false
|
|
label.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ ProfileLayoutEnforcement.applyLeftAlignedTextField(label)
|
|
|
|
|
|
|
|
careerField.translatesAutoresizingMaskIntoConstraints = false
|
|
careerField.translatesAutoresizingMaskIntoConstraints = false
|
|
|
careerField.isEditable = true
|
|
careerField.isEditable = true
|
|
@@ -307,15 +473,15 @@ final class MyProfilePageView: NSView {
|
|
|
careerField.stringValue = ""
|
|
careerField.stringValue = ""
|
|
|
careerField.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
careerField.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
careerField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
careerField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
|
- careerField.baseWritingDirection = .leftToRight
|
|
|
|
|
- careerField.alignment = .left
|
|
|
|
|
careerField.placeholderAttributedString = NSAttributedString(
|
|
careerField.placeholderAttributedString = NSAttributedString(
|
|
|
string: "Brief overview of your professional background and key achievements...",
|
|
string: "Brief overview of your professional background and key achievements...",
|
|
|
attributes: [
|
|
attributes: [
|
|
|
.foregroundColor: ProfilePagePalette.secondaryText,
|
|
.foregroundColor: ProfilePagePalette.secondaryText,
|
|
|
- .font: NSFont.systemFont(ofSize: 14, weight: .regular)
|
|
|
|
|
|
|
+ .font: NSFont.systemFont(ofSize: 14, weight: .regular),
|
|
|
|
|
+ .paragraphStyle: ProfileLayoutEnforcement.leftAlignedParagraphStyle()
|
|
|
]
|
|
]
|
|
|
)
|
|
)
|
|
|
|
|
+ ProfileLayoutEnforcement.applyLeftAlignedTextField(careerField)
|
|
|
|
|
|
|
|
let wrap = NSView()
|
|
let wrap = NSView()
|
|
|
wrap.translatesAutoresizingMaskIntoConstraints = false
|
|
wrap.translatesAutoresizingMaskIntoConstraints = false
|
|
@@ -328,12 +494,13 @@ final class MyProfilePageView: NSView {
|
|
|
wrap.layer?.cornerCurve = .continuous
|
|
wrap.layer?.cornerCurve = .continuous
|
|
|
}
|
|
}
|
|
|
wrap.addSubview(careerField)
|
|
wrap.addSubview(careerField)
|
|
|
|
|
+ ProfileLayoutEnforcement.applyForcedLTR(to: wrap)
|
|
|
NSLayoutConstraint.activate([
|
|
NSLayoutConstraint.activate([
|
|
|
careerField.leftAnchor.constraint(equalTo: wrap.leftAnchor, constant: 12),
|
|
careerField.leftAnchor.constraint(equalTo: wrap.leftAnchor, constant: 12),
|
|
|
careerField.rightAnchor.constraint(equalTo: wrap.rightAnchor, constant: -12),
|
|
careerField.rightAnchor.constraint(equalTo: wrap.rightAnchor, constant: -12),
|
|
|
careerField.topAnchor.constraint(equalTo: wrap.topAnchor, constant: 10),
|
|
careerField.topAnchor.constraint(equalTo: wrap.topAnchor, constant: 10),
|
|
|
careerField.bottomAnchor.constraint(equalTo: wrap.bottomAnchor, constant: -10),
|
|
careerField.bottomAnchor.constraint(equalTo: wrap.bottomAnchor, constant: -10),
|
|
|
- wrap.heightAnchor.constraint(greaterThanOrEqualToConstant: 120)
|
|
|
|
|
|
|
+ wrap.heightAnchor.constraint(greaterThanOrEqualToConstant: 168)
|
|
|
])
|
|
])
|
|
|
|
|
|
|
|
let stack = NSStackView(views: [label, wrap])
|
|
let stack = NSStackView(views: [label, wrap])
|
|
@@ -342,16 +509,296 @@ final class MyProfilePageView: NSView {
|
|
|
stack.alignment = .width
|
|
stack.alignment = .width
|
|
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
stack.userInterfaceLayoutDirection = .leftToRight
|
|
stack.userInterfaceLayoutDirection = .leftToRight
|
|
|
|
|
+ ProfileLayoutEnforcement.applyForcedLTR(to: stack)
|
|
|
stack.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
stack.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
wrap.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
wrap.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
return stack
|
|
return stack
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private func multilineProfileBlock(title: String, placeholder: String, field: NSTextField, minHeight: CGFloat) -> NSView {
|
|
|
|
|
+ let label = NSTextField(labelWithString: title)
|
|
|
|
|
+ label.font = .systemFont(ofSize: 12, weight: .medium)
|
|
|
|
|
+ label.textColor = ProfilePagePalette.secondaryText
|
|
|
|
|
+ label.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ ProfileLayoutEnforcement.applyLeftAlignedTextField(label)
|
|
|
|
|
+
|
|
|
|
|
+ field.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ field.isEditable = true
|
|
|
|
|
+ field.isSelectable = true
|
|
|
|
|
+ field.isBordered = false
|
|
|
|
|
+ field.drawsBackground = false
|
|
|
|
|
+ field.focusRingType = .none
|
|
|
|
|
+ field.font = .systemFont(ofSize: 14, weight: .regular)
|
|
|
|
|
+ field.textColor = ProfilePagePalette.primaryText
|
|
|
|
|
+ field.maximumNumberOfLines = 0
|
|
|
|
|
+ field.cell?.wraps = true
|
|
|
|
|
+ field.cell?.isScrollable = false
|
|
|
|
|
+ field.cell?.usesSingleLineMode = false
|
|
|
|
|
+ field.stringValue = ""
|
|
|
|
|
+ field.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
|
|
+ field.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
|
|
|
+ field.placeholderAttributedString = NSAttributedString(
|
|
|
|
|
+ string: placeholder,
|
|
|
|
|
+ attributes: [
|
|
|
|
|
+ .foregroundColor: ProfilePagePalette.secondaryText,
|
|
|
|
|
+ .font: NSFont.systemFont(ofSize: 14, weight: .regular),
|
|
|
|
|
+ .paragraphStyle: ProfileLayoutEnforcement.leftAlignedParagraphStyle()
|
|
|
|
|
+ ]
|
|
|
|
|
+ )
|
|
|
|
|
+ ProfileLayoutEnforcement.applyLeftAlignedTextField(field)
|
|
|
|
|
+
|
|
|
|
|
+ let wrap = NSView()
|
|
|
|
|
+ wrap.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ wrap.wantsLayer = true
|
|
|
|
|
+ wrap.layer?.backgroundColor = ProfilePagePalette.fieldFill.cgColor
|
|
|
|
|
+ wrap.layer?.cornerRadius = 10
|
|
|
|
|
+ wrap.layer?.borderWidth = 1
|
|
|
|
|
+ wrap.layer?.borderColor = ProfilePagePalette.border.cgColor
|
|
|
|
|
+ if #available(macOS 11.0, *) {
|
|
|
|
|
+ wrap.layer?.cornerCurve = .continuous
|
|
|
|
|
+ }
|
|
|
|
|
+ wrap.addSubview(field)
|
|
|
|
|
+ ProfileLayoutEnforcement.applyForcedLTR(to: wrap)
|
|
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
|
|
+ field.leftAnchor.constraint(equalTo: wrap.leftAnchor, constant: 12),
|
|
|
|
|
+ field.rightAnchor.constraint(equalTo: wrap.rightAnchor, constant: -12),
|
|
|
|
|
+ field.topAnchor.constraint(equalTo: wrap.topAnchor, constant: 10),
|
|
|
|
|
+ field.bottomAnchor.constraint(equalTo: wrap.bottomAnchor, constant: -10),
|
|
|
|
|
+ wrap.heightAnchor.constraint(greaterThanOrEqualToConstant: minHeight)
|
|
|
|
|
+ ])
|
|
|
|
|
+
|
|
|
|
|
+ let stack = NSStackView(views: [label, wrap])
|
|
|
|
|
+ stack.orientation = .vertical
|
|
|
|
|
+ stack.spacing = 8
|
|
|
|
|
+ stack.alignment = .width
|
|
|
|
|
+ stack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ stack.userInterfaceLayoutDirection = .leftToRight
|
|
|
|
|
+ ProfileLayoutEnforcement.applyForcedLTR(to: stack)
|
|
|
|
|
+ stack.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
|
|
+ wrap.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
|
|
+ return stack
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func referralBlock() -> NSView {
|
|
|
|
|
+ let label = NSTextField(labelWithString: "Referral (Optional)")
|
|
|
|
|
+ label.font = .systemFont(ofSize: 12, weight: .medium)
|
|
|
|
|
+ label.textColor = ProfilePagePalette.secondaryText
|
|
|
|
|
+ label.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ ProfileLayoutEnforcement.applyLeftAlignedTextField(label)
|
|
|
|
|
+
|
|
|
|
|
+ styleSingleLineField(referralField, placeholder: "Referred by (Company/Person Name)")
|
|
|
|
|
+ let wrap = roundedFieldChrome(containing: referralField, minHeight: 40)
|
|
|
|
|
+
|
|
|
|
|
+ let helper = NSTextField(wrappingLabelWithString: "If someone referred you for this job, enter their name or company here")
|
|
|
|
|
+ helper.font = .systemFont(ofSize: 11, weight: .regular)
|
|
|
|
|
+ helper.textColor = ProfilePagePalette.secondaryText
|
|
|
|
|
+ helper.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ ProfileLayoutEnforcement.applyLeftAlignedTextField(helper)
|
|
|
|
|
+ referralHelperLabel = helper
|
|
|
|
|
+
|
|
|
|
|
+ let stack = NSStackView(views: [label, wrap, helper])
|
|
|
|
|
+ stack.orientation = .vertical
|
|
|
|
|
+ stack.spacing = 8
|
|
|
|
|
+ stack.alignment = .width
|
|
|
|
|
+ stack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ stack.userInterfaceLayoutDirection = .leftToRight
|
|
|
|
|
+ ProfileLayoutEnforcement.applyForcedLTR(to: stack)
|
|
|
|
|
+ stack.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
|
|
+ wrap.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
|
|
+ if #available(macOS 10.11, *) {
|
|
|
|
|
+ stack.setCustomSpacing(6, after: wrap)
|
|
|
|
|
+ }
|
|
|
|
|
+ return stack
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func horizontalSeparator() -> NSView {
|
|
|
|
|
+ let box = NSBox()
|
|
|
|
|
+ box.boxType = .separator
|
|
|
|
|
+ box.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ return box
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func workExperienceSection() -> NSView {
|
|
|
|
|
+ let title = NSTextField(labelWithString: "Work Experience")
|
|
|
|
|
+ title.font = .systemFont(ofSize: 15, weight: .semibold)
|
|
|
|
|
+ title.textColor = ProfilePagePalette.primaryText
|
|
|
|
|
+ title.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ title.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
|
|
+ ProfileLayoutEnforcement.applyLeftAlignedTextField(title)
|
|
|
|
|
+
|
|
|
|
|
+ let addButton = NSButton(title: "+ Add Another", target: self, action: #selector(didTapAddWorkExperience))
|
|
|
|
|
+ addButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ addButton.bezelStyle = .rounded
|
|
|
|
|
+ addButton.isBordered = true
|
|
|
|
|
+ addButton.font = .systemFont(ofSize: 12, weight: .medium)
|
|
|
|
|
+ addButton.controlSize = .regular
|
|
|
|
|
+ addButton.setContentHuggingPriority(.required, for: .horizontal)
|
|
|
|
|
+
|
|
|
|
|
+ let spacer = NSView()
|
|
|
|
|
+ spacer.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ spacer.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
|
|
+
|
|
|
|
|
+ let headerRow = NSStackView(views: [title, spacer, addButton])
|
|
|
|
|
+ headerRow.orientation = .horizontal
|
|
|
|
|
+ headerRow.alignment = .centerY
|
|
|
|
|
+ headerRow.distribution = .fill
|
|
|
|
|
+ headerRow.spacing = 12
|
|
|
|
|
+ headerRow.userInterfaceLayoutDirection = .leftToRight
|
|
|
|
|
+ ProfileLayoutEnforcement.applyForcedLTR(to: headerRow)
|
|
|
|
|
+ headerRow.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+
|
|
|
|
|
+ workExperienceRowsStack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ workExperienceRowsStack.orientation = .vertical
|
|
|
|
|
+ workExperienceRowsStack.spacing = 20
|
|
|
|
|
+ workExperienceRowsStack.alignment = .width
|
|
|
|
|
+ workExperienceRowsStack.userInterfaceLayoutDirection = .leftToRight
|
|
|
|
|
+ ProfileLayoutEnforcement.applyForcedLTR(to: workExperienceRowsStack)
|
|
|
|
|
+
|
|
|
|
|
+ let outer = NSStackView(views: [headerRow, workExperienceRowsStack])
|
|
|
|
|
+ outer.orientation = .vertical
|
|
|
|
|
+ outer.spacing = 16
|
|
|
|
|
+ outer.alignment = .width
|
|
|
|
|
+ outer.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ outer.userInterfaceLayoutDirection = .leftToRight
|
|
|
|
|
+ ProfileLayoutEnforcement.applyForcedLTR(to: outer)
|
|
|
|
|
+ return outer
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func educationSection() -> NSView {
|
|
|
|
|
+ let title = NSTextField(labelWithString: "Education")
|
|
|
|
|
+ title.font = .systemFont(ofSize: 15, weight: .semibold)
|
|
|
|
|
+ title.textColor = ProfilePagePalette.primaryText
|
|
|
|
|
+ title.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ title.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
|
|
+ ProfileLayoutEnforcement.applyLeftAlignedTextField(title)
|
|
|
|
|
+
|
|
|
|
|
+ let addButton = NSButton(title: "+ Add Another", target: self, action: #selector(didTapAddEducation))
|
|
|
|
|
+ addButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ addButton.bezelStyle = .rounded
|
|
|
|
|
+ addButton.isBordered = true
|
|
|
|
|
+ addButton.font = .systemFont(ofSize: 12, weight: .medium)
|
|
|
|
|
+ addButton.setContentHuggingPriority(.required, for: .horizontal)
|
|
|
|
|
+
|
|
|
|
|
+ let spacer = NSView()
|
|
|
|
|
+ spacer.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ spacer.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
|
|
+
|
|
|
|
|
+ let headerRow = NSStackView(views: [title, spacer, addButton])
|
|
|
|
|
+ headerRow.orientation = .horizontal
|
|
|
|
|
+ headerRow.alignment = .centerY
|
|
|
|
|
+ headerRow.distribution = .fill
|
|
|
|
|
+ headerRow.spacing = 12
|
|
|
|
|
+ headerRow.userInterfaceLayoutDirection = .leftToRight
|
|
|
|
|
+ ProfileLayoutEnforcement.applyForcedLTR(to: headerRow)
|
|
|
|
|
+ headerRow.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+
|
|
|
|
|
+ educationRowsStack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ educationRowsStack.orientation = .vertical
|
|
|
|
|
+ educationRowsStack.spacing = 16
|
|
|
|
|
+ educationRowsStack.alignment = .width
|
|
|
|
|
+ educationRowsStack.userInterfaceLayoutDirection = .leftToRight
|
|
|
|
|
+ ProfileLayoutEnforcement.applyForcedLTR(to: educationRowsStack)
|
|
|
|
|
+
|
|
|
|
|
+ let outer = NSStackView(views: [headerRow, educationRowsStack])
|
|
|
|
|
+ outer.orientation = .vertical
|
|
|
|
|
+ outer.spacing = 16
|
|
|
|
|
+ outer.alignment = .width
|
|
|
|
|
+ outer.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ outer.userInterfaceLayoutDirection = .leftToRight
|
|
|
|
|
+ ProfileLayoutEnforcement.applyForcedLTR(to: outer)
|
|
|
|
|
+ return outer
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func appendWorkExperienceEntry() {
|
|
|
|
|
+ let entry = WorkExperienceEntryView()
|
|
|
|
|
+ entry.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ if let compact = lastCompactLayout {
|
|
|
|
|
+ entry.applyCompactLayout(compact)
|
|
|
|
|
+ }
|
|
|
|
|
+ entry.onDelete = { [weak self, weak entry] in
|
|
|
|
|
+ guard let self, let entry else { return }
|
|
|
|
|
+ self.removeWorkExperienceEntry(entry)
|
|
|
|
|
+ }
|
|
|
|
|
+ workExperienceEntries.append(entry)
|
|
|
|
|
+ workExperienceRowsStack.addArrangedSubview(entry)
|
|
|
|
|
+ renumberWorkExperienceEntries()
|
|
|
|
|
+ refreshWorkExperienceDeleteButtons()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func removeWorkExperienceEntry(_ entry: WorkExperienceEntryView) {
|
|
|
|
|
+ guard workExperienceEntries.count > 1 else { return }
|
|
|
|
|
+ workExperienceEntries.removeAll { $0 === entry }
|
|
|
|
|
+ workExperienceRowsStack.removeArrangedSubview(entry)
|
|
|
|
|
+ entry.removeFromSuperview()
|
|
|
|
|
+ renumberWorkExperienceEntries()
|
|
|
|
|
+ refreshWorkExperienceDeleteButtons()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func renumberWorkExperienceEntries() {
|
|
|
|
|
+ for (i, entry) in workExperienceEntries.enumerated() {
|
|
|
|
|
+ entry.setExperienceIndex(i + 1)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func refreshWorkExperienceDeleteButtons() {
|
|
|
|
|
+ let hide = workExperienceEntries.count <= 1
|
|
|
|
|
+ for entry in workExperienceEntries {
|
|
|
|
|
+ entry.setDeleteHidden(hide)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func appendEducationEntry() {
|
|
|
|
|
+ let entry = EducationEntryView()
|
|
|
|
|
+ entry.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ if let compact = lastCompactLayout {
|
|
|
|
|
+ entry.applyCompactLayout(compact)
|
|
|
|
|
+ }
|
|
|
|
|
+ entry.onDelete = { [weak self, weak entry] in
|
|
|
|
|
+ guard let self, let entry else { return }
|
|
|
|
|
+ self.removeEducationEntry(entry)
|
|
|
|
|
+ }
|
|
|
|
|
+ educationEntries.append(entry)
|
|
|
|
|
+ educationRowsStack.addArrangedSubview(entry)
|
|
|
|
|
+ renumberEducationEntries()
|
|
|
|
|
+ refreshEducationDeleteButtons()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func removeEducationEntry(_ entry: EducationEntryView) {
|
|
|
|
|
+ guard educationEntries.count > 1 else { return }
|
|
|
|
|
+ educationEntries.removeAll { $0 === entry }
|
|
|
|
|
+ educationRowsStack.removeArrangedSubview(entry)
|
|
|
|
|
+ entry.removeFromSuperview()
|
|
|
|
|
+ renumberEducationEntries()
|
|
|
|
|
+ refreshEducationDeleteButtons()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func renumberEducationEntries() {
|
|
|
|
|
+ for (i, entry) in educationEntries.enumerated() {
|
|
|
|
|
+ entry.setEducationIndex(i + 1)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func refreshEducationDeleteButtons() {
|
|
|
|
|
+ let hide = educationEntries.count <= 1
|
|
|
|
|
+ for entry in educationEntries {
|
|
|
|
|
+ entry.setDeleteHidden(hide)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @objc private func didTapAddWorkExperience() {
|
|
|
|
|
+ appendWorkExperienceEntry()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @objc private func didTapAddEducation() {
|
|
|
|
|
+ appendEducationEntry()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private func saveButtonHost() -> NSView {
|
|
private func saveButtonHost() -> NSView {
|
|
|
saveButton.translatesAutoresizingMaskIntoConstraints = false
|
|
saveButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
let host = NSView()
|
|
let host = NSView()
|
|
|
host.translatesAutoresizingMaskIntoConstraints = false
|
|
host.translatesAutoresizingMaskIntoConstraints = false
|
|
|
host.userInterfaceLayoutDirection = .leftToRight
|
|
host.userInterfaceLayoutDirection = .leftToRight
|
|
|
|
|
+ ProfileLayoutEnforcement.applyForcedLTR(to: host)
|
|
|
host.addSubview(saveButton)
|
|
host.addSubview(saveButton)
|
|
|
NSLayoutConstraint.activate([
|
|
NSLayoutConstraint.activate([
|
|
|
saveButton.leadingAnchor.constraint(equalTo: host.leadingAnchor),
|
|
saveButton.leadingAnchor.constraint(equalTo: host.leadingAnchor),
|
|
@@ -368,6 +815,438 @@ final class MyProfilePageView: NSView {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// MARK: - Work experience & education rows
|
|
|
|
|
+
|
|
|
|
|
+private final class WorkExperienceEntryView: NSView {
|
|
|
|
|
+ var onDelete: (() -> Void)?
|
|
|
|
|
+
|
|
|
|
|
+ private let subtitleLabel = NSTextField(labelWithString: "Experience 1")
|
|
|
|
|
+ private let deleteButton = NSButton()
|
|
|
|
|
+ private let jobTitleField = NSTextField()
|
|
|
|
|
+ private let companyField = NSTextField()
|
|
|
|
|
+ private let durationField = NSTextField()
|
|
|
|
|
+ private let descriptionField = NSTextField()
|
|
|
|
|
+ private let jobCompanyRow = NSStackView()
|
|
|
|
|
+
|
|
|
|
|
+ override init(frame frameRect: NSRect) {
|
|
|
|
|
+ super.init(frame: frameRect)
|
|
|
|
|
+ configure()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ required init?(coder: NSCoder) {
|
|
|
|
|
+ fatalError("init(coder:) has not been implemented")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ func setExperienceIndex(_ index: Int) {
|
|
|
|
|
+ subtitleLabel.stringValue = "Experience \(index)"
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ func setDeleteHidden(_ hidden: Bool) {
|
|
|
|
|
+ deleteButton.isHidden = hidden
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ func applyCompactLayout(_ compact: Bool) {
|
|
|
|
|
+ jobCompanyRow.orientation = compact ? .vertical : .horizontal
|
|
|
|
|
+ jobCompanyRow.spacing = compact ? 12 : 12
|
|
|
|
|
+ jobCompanyRow.distribution = compact ? .fill : .fillEqually
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func configure() {
|
|
|
|
|
+ userInterfaceLayoutDirection = .leftToRight
|
|
|
|
|
+ ProfileLayoutEnforcement.applyForcedLTR(to: self)
|
|
|
|
|
+ wantsLayer = true
|
|
|
|
|
+ layer?.cornerRadius = 14
|
|
|
|
|
+ layer?.borderWidth = 1
|
|
|
|
|
+ layer?.borderColor = ProfilePagePalette.border.cgColor
|
|
|
|
|
+ layer?.backgroundColor = ProfilePagePalette.cardBackground.cgColor
|
|
|
|
|
+ layer?.masksToBounds = false
|
|
|
|
|
+ layer?.shadowColor = NSColor.black.cgColor
|
|
|
|
|
+ layer?.shadowOpacity = 0.05
|
|
|
|
|
+ layer?.shadowRadius = 12
|
|
|
|
|
+ layer?.shadowOffset = CGSize(width: 0, height: 6)
|
|
|
|
|
+ if #available(macOS 11.0, *) {
|
|
|
|
|
+ layer?.cornerCurve = .continuous
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ subtitleLabel.font = .systemFont(ofSize: 12, weight: .medium)
|
|
|
|
|
+ subtitleLabel.textColor = ProfilePagePalette.secondaryText
|
|
|
|
|
+ subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ ProfileLayoutEnforcement.applyLeftAlignedTextField(subtitleLabel)
|
|
|
|
|
+
|
|
|
|
|
+ deleteButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ deleteButton.isBordered = false
|
|
|
|
|
+ deleteButton.bezelStyle = .regularSquare
|
|
|
|
|
+ deleteButton.focusRingType = .none
|
|
|
|
|
+ deleteButton.contentTintColor = ProfilePagePalette.destructive
|
|
|
|
|
+ deleteButton.target = self
|
|
|
|
|
+ deleteButton.action = #selector(didTapDelete)
|
|
|
|
|
+ if #available(macOS 11.0, *) {
|
|
|
|
|
+ deleteButton.image = NSImage(systemSymbolName: "trash", accessibilityDescription: "Remove experience")
|
|
|
|
|
+ deleteButton.imagePosition = .imageOnly
|
|
|
|
|
+ } else {
|
|
|
|
|
+ deleteButton.title = "Remove"
|
|
|
|
|
+ deleteButton.font = .systemFont(ofSize: 12, weight: .medium)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let headerSpacer = NSView()
|
|
|
|
|
+ headerSpacer.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ headerSpacer.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
|
|
+ let headerRow = NSStackView(views: [subtitleLabel, headerSpacer, deleteButton])
|
|
|
|
|
+ headerRow.orientation = .horizontal
|
|
|
|
|
+ headerRow.alignment = .centerY
|
|
|
|
|
+ headerRow.distribution = .fill
|
|
|
|
|
+ headerRow.spacing = 8
|
|
|
|
|
+ headerRow.userInterfaceLayoutDirection = .leftToRight
|
|
|
|
|
+ ProfileLayoutEnforcement.applyForcedLTR(to: headerRow)
|
|
|
|
|
+ headerRow.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+
|
|
|
|
|
+ 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")
|
|
|
|
|
+ configureTwoColumnRow(jobCompanyRow, left: jobGroup, right: companyGroup)
|
|
|
|
|
+ pinEqualColumnWidths(in: jobCompanyRow)
|
|
|
|
|
+
|
|
|
|
|
+ let durationGroup = Self.labeledFieldStack(title: "Duration *", field: durationField, placeholder: "e.g., Jan 2020 - Present")
|
|
|
|
|
+ let descriptionGroup = Self.multilineLabeledStack(
|
|
|
|
|
+ title: "Description",
|
|
|
|
|
+ field: descriptionField,
|
|
|
|
|
+ placeholder: "Describe your responsibilities and achievements...",
|
|
|
|
|
+ minHeight: 120
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ let inner = NSStackView(views: [headerRow, jobCompanyRow, durationGroup, descriptionGroup])
|
|
|
|
|
+ inner.orientation = .vertical
|
|
|
|
|
+ inner.spacing = 16
|
|
|
|
|
+ inner.alignment = .width
|
|
|
|
|
+ inner.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ inner.edgeInsets = NSEdgeInsets(top: 16, left: 18, bottom: 16, right: 18)
|
|
|
|
|
+ inner.userInterfaceLayoutDirection = .leftToRight
|
|
|
|
|
+ ProfileLayoutEnforcement.applyForcedLTR(to: inner)
|
|
|
|
|
+
|
|
|
|
|
+ addSubview(inner)
|
|
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
|
|
+ inner.leftAnchor.constraint(equalTo: leftAnchor),
|
|
|
|
|
+ inner.rightAnchor.constraint(equalTo: rightAnchor),
|
|
|
|
|
+ inner.topAnchor.constraint(equalTo: topAnchor),
|
|
|
|
|
+ inner.bottomAnchor.constraint(equalTo: bottomAnchor)
|
|
|
|
|
+ ])
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ 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() {
|
|
|
|
|
+ super.layout()
|
|
|
|
|
+ for field in [jobTitleField, companyField, durationField, descriptionField] {
|
|
|
|
|
+ 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 }
|
|
|
|
|
+ let r = layer.cornerRadius
|
|
|
|
|
+ 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() {
|
|
|
|
|
+ onDelete?()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ fileprivate static func labeledFieldStack(title: String, field: NSTextField, placeholder: String) -> NSView {
|
|
|
|
|
+ let label = NSTextField(labelWithString: title)
|
|
|
|
|
+ label.font = .systemFont(ofSize: 12, weight: .medium)
|
|
|
|
|
+ label.textColor = ProfilePagePalette.secondaryText
|
|
|
|
|
+ label.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ ProfileLayoutEnforcement.applyLeftAlignedTextField(label)
|
|
|
|
|
+
|
|
|
|
|
+ styleSingleLineField(field, placeholder: placeholder)
|
|
|
|
|
+ let wrap = roundedChrome(around: field, minHeight: 40)
|
|
|
|
|
+
|
|
|
|
|
+ let stack = NSStackView(views: [label, wrap])
|
|
|
|
|
+ stack.orientation = .vertical
|
|
|
|
|
+ stack.spacing = 8
|
|
|
|
|
+ stack.alignment = .width
|
|
|
|
|
+ stack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ stack.userInterfaceLayoutDirection = .leftToRight
|
|
|
|
|
+ ProfileLayoutEnforcement.applyForcedLTR(to: stack)
|
|
|
|
|
+ NSLayoutConstraint.activate([wrap.widthAnchor.constraint(equalTo: stack.widthAnchor)])
|
|
|
|
|
+ return stack
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static func multilineLabeledStack(title: String, field: NSTextField, placeholder: String, minHeight: CGFloat) -> NSView {
|
|
|
|
|
+ let label = NSTextField(labelWithString: title)
|
|
|
|
|
+ label.font = .systemFont(ofSize: 12, weight: .medium)
|
|
|
|
|
+ label.textColor = ProfilePagePalette.secondaryText
|
|
|
|
|
+ label.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ ProfileLayoutEnforcement.applyLeftAlignedTextField(label)
|
|
|
|
|
+
|
|
|
|
|
+ field.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ field.isEditable = true
|
|
|
|
|
+ field.isSelectable = true
|
|
|
|
|
+ field.isBordered = false
|
|
|
|
|
+ field.drawsBackground = false
|
|
|
|
|
+ field.focusRingType = .none
|
|
|
|
|
+ field.font = .systemFont(ofSize: 14, weight: .regular)
|
|
|
|
|
+ field.textColor = ProfilePagePalette.primaryText
|
|
|
|
|
+ field.maximumNumberOfLines = 0
|
|
|
|
|
+ field.cell?.wraps = true
|
|
|
|
|
+ field.cell?.isScrollable = false
|
|
|
|
|
+ field.cell?.usesSingleLineMode = false
|
|
|
|
|
+ field.placeholderAttributedString = NSAttributedString(
|
|
|
|
|
+ string: placeholder,
|
|
|
|
|
+ attributes: [
|
|
|
|
|
+ .foregroundColor: ProfilePagePalette.secondaryText,
|
|
|
|
|
+ .font: NSFont.systemFont(ofSize: 14, weight: .regular),
|
|
|
|
|
+ .paragraphStyle: ProfileLayoutEnforcement.leftAlignedParagraphStyle()
|
|
|
|
|
+ ]
|
|
|
|
|
+ )
|
|
|
|
|
+ ProfileLayoutEnforcement.applyLeftAlignedTextField(field)
|
|
|
|
|
+
|
|
|
|
|
+ let wrap = NSView()
|
|
|
|
|
+ wrap.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ wrap.wantsLayer = true
|
|
|
|
|
+ wrap.layer?.backgroundColor = ProfilePagePalette.fieldFill.cgColor
|
|
|
|
|
+ wrap.layer?.cornerRadius = 10
|
|
|
|
|
+ wrap.layer?.borderWidth = 1
|
|
|
|
|
+ wrap.layer?.borderColor = ProfilePagePalette.border.cgColor
|
|
|
|
|
+ if #available(macOS 11.0, *) {
|
|
|
|
|
+ wrap.layer?.cornerCurve = .continuous
|
|
|
|
|
+ }
|
|
|
|
|
+ wrap.addSubview(field)
|
|
|
|
|
+ ProfileLayoutEnforcement.applyForcedLTR(to: wrap)
|
|
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
|
|
+ field.leftAnchor.constraint(equalTo: wrap.leftAnchor, constant: 12),
|
|
|
|
|
+ field.rightAnchor.constraint(equalTo: wrap.rightAnchor, constant: -12),
|
|
|
|
|
+ field.topAnchor.constraint(equalTo: wrap.topAnchor, constant: 10),
|
|
|
|
|
+ field.bottomAnchor.constraint(equalTo: wrap.bottomAnchor, constant: -10),
|
|
|
|
|
+ wrap.heightAnchor.constraint(greaterThanOrEqualToConstant: minHeight)
|
|
|
|
|
+ ])
|
|
|
|
|
+
|
|
|
|
|
+ let stack = NSStackView(views: [label, wrap])
|
|
|
|
|
+ stack.orientation = .vertical
|
|
|
|
|
+ stack.spacing = 8
|
|
|
|
|
+ stack.alignment = .width
|
|
|
|
|
+ stack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ stack.userInterfaceLayoutDirection = .leftToRight
|
|
|
|
|
+ ProfileLayoutEnforcement.applyForcedLTR(to: stack)
|
|
|
|
|
+ return stack
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static func styleSingleLineField(_ field: NSTextField, placeholder: String) {
|
|
|
|
|
+ field.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ field.isBordered = false
|
|
|
|
|
+ field.drawsBackground = false
|
|
|
|
|
+ field.focusRingType = .none
|
|
|
|
|
+ field.font = .systemFont(ofSize: 14, weight: .regular)
|
|
|
|
|
+ field.textColor = ProfilePagePalette.primaryText
|
|
|
|
|
+ field.placeholderAttributedString = NSAttributedString(
|
|
|
|
|
+ string: placeholder,
|
|
|
|
|
+ attributes: [
|
|
|
|
|
+ .foregroundColor: ProfilePagePalette.secondaryText,
|
|
|
|
|
+ .font: NSFont.systemFont(ofSize: 14, weight: .regular),
|
|
|
|
|
+ .paragraphStyle: ProfileLayoutEnforcement.leftAlignedParagraphStyle()
|
|
|
|
|
+ ]
|
|
|
|
|
+ )
|
|
|
|
|
+ field.cell?.usesSingleLineMode = true
|
|
|
|
|
+ field.cell?.wraps = false
|
|
|
|
|
+ field.cell?.isScrollable = true
|
|
|
|
|
+ ProfileLayoutEnforcement.applyLeftAlignedTextField(field)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static func roundedChrome(around field: NSTextField, minHeight: CGFloat) -> NSView {
|
|
|
|
|
+ let wrap = NSView()
|
|
|
|
|
+ wrap.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ wrap.wantsLayer = true
|
|
|
|
|
+ wrap.layer?.backgroundColor = ProfilePagePalette.fieldFill.cgColor
|
|
|
|
|
+ wrap.layer?.cornerRadius = 10
|
|
|
|
|
+ wrap.layer?.borderWidth = 1
|
|
|
|
|
+ wrap.layer?.borderColor = ProfilePagePalette.border.cgColor
|
|
|
|
|
+ if #available(macOS 11.0, *) {
|
|
|
|
|
+ wrap.layer?.cornerCurve = .continuous
|
|
|
|
|
+ }
|
|
|
|
|
+ wrap.addSubview(field)
|
|
|
|
|
+ ProfileLayoutEnforcement.applyForcedLTR(to: wrap)
|
|
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
|
|
+ field.leftAnchor.constraint(equalTo: wrap.leftAnchor, constant: 12),
|
|
|
|
|
+ field.rightAnchor.constraint(equalTo: wrap.rightAnchor, constant: -12),
|
|
|
|
|
+ field.centerYAnchor.constraint(equalTo: wrap.centerYAnchor),
|
|
|
|
|
+ wrap.heightAnchor.constraint(greaterThanOrEqualToConstant: minHeight)
|
|
|
|
|
+ ])
|
|
|
|
|
+ return wrap
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+private final class EducationEntryView: NSView {
|
|
|
|
|
+ var onDelete: (() -> Void)?
|
|
|
|
|
+
|
|
|
|
|
+ private let subtitleLabel = NSTextField(labelWithString: "Education 1")
|
|
|
|
|
+ private let deleteButton = NSButton()
|
|
|
|
|
+ private let degreeField = NSTextField()
|
|
|
|
|
+ private let institutionField = NSTextField()
|
|
|
|
|
+ private let yearField = NSTextField()
|
|
|
|
|
+ private let degreeInstitutionRow = NSStackView()
|
|
|
|
|
+
|
|
|
|
|
+ override init(frame frameRect: NSRect) {
|
|
|
|
|
+ super.init(frame: frameRect)
|
|
|
|
|
+ configure()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ required init?(coder: NSCoder) {
|
|
|
|
|
+ fatalError("init(coder:) has not been implemented")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ func setEducationIndex(_ index: Int) {
|
|
|
|
|
+ subtitleLabel.stringValue = "Education \(index)"
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ func setDeleteHidden(_ hidden: Bool) {
|
|
|
|
|
+ deleteButton.isHidden = hidden
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ func applyCompactLayout(_ compact: Bool) {
|
|
|
|
|
+ degreeInstitutionRow.orientation = compact ? .vertical : .horizontal
|
|
|
|
|
+ degreeInstitutionRow.spacing = 12
|
|
|
|
|
+ degreeInstitutionRow.distribution = compact ? .fill : .fillEqually
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func configure() {
|
|
|
|
|
+ userInterfaceLayoutDirection = .leftToRight
|
|
|
|
|
+ ProfileLayoutEnforcement.applyForcedLTR(to: self)
|
|
|
|
|
+ wantsLayer = true
|
|
|
|
|
+ layer?.cornerRadius = 14
|
|
|
|
|
+ layer?.borderWidth = 1
|
|
|
|
|
+ layer?.borderColor = ProfilePagePalette.border.cgColor
|
|
|
|
|
+ layer?.backgroundColor = ProfilePagePalette.cardBackground.cgColor
|
|
|
|
|
+ layer?.masksToBounds = false
|
|
|
|
|
+ layer?.shadowColor = NSColor.black.cgColor
|
|
|
|
|
+ layer?.shadowOpacity = 0.04
|
|
|
|
|
+ layer?.shadowRadius = 10
|
|
|
|
|
+ layer?.shadowOffset = CGSize(width: 0, height: 5)
|
|
|
|
|
+ if #available(macOS 11.0, *) {
|
|
|
|
|
+ layer?.cornerCurve = .continuous
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ subtitleLabel.font = .systemFont(ofSize: 12, weight: .medium)
|
|
|
|
|
+ subtitleLabel.textColor = ProfilePagePalette.secondaryText
|
|
|
|
|
+ subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ ProfileLayoutEnforcement.applyLeftAlignedTextField(subtitleLabel)
|
|
|
|
|
+
|
|
|
|
|
+ deleteButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ deleteButton.isBordered = false
|
|
|
|
|
+ deleteButton.bezelStyle = .regularSquare
|
|
|
|
|
+ deleteButton.focusRingType = .none
|
|
|
|
|
+ deleteButton.contentTintColor = ProfilePagePalette.destructive
|
|
|
|
|
+ deleteButton.target = self
|
|
|
|
|
+ deleteButton.action = #selector(didTapDelete)
|
|
|
|
|
+ if #available(macOS 11.0, *) {
|
|
|
|
|
+ deleteButton.image = NSImage(systemSymbolName: "trash", accessibilityDescription: "Remove education")
|
|
|
|
|
+ deleteButton.imagePosition = .imageOnly
|
|
|
|
|
+ } else {
|
|
|
|
|
+ deleteButton.title = "Remove"
|
|
|
|
|
+ deleteButton.font = .systemFont(ofSize: 12, weight: .medium)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let headerSpacer = NSView()
|
|
|
|
|
+ headerSpacer.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ headerSpacer.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
|
|
+ let headerRow = NSStackView(views: [subtitleLabel, headerSpacer, deleteButton])
|
|
|
|
|
+ headerRow.orientation = .horizontal
|
|
|
|
|
+ headerRow.alignment = .centerY
|
|
|
|
|
+ headerRow.distribution = .fill
|
|
|
|
|
+ headerRow.spacing = 8
|
|
|
|
|
+ headerRow.userInterfaceLayoutDirection = .leftToRight
|
|
|
|
|
+ ProfileLayoutEnforcement.applyForcedLTR(to: headerRow)
|
|
|
|
|
+ headerRow.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+
|
|
|
|
|
+ let degreeGroup = WorkExperienceEntryView.labeledFieldStack(
|
|
|
|
|
+ title: "Degree / program *",
|
|
|
|
|
+ field: degreeField,
|
|
|
|
|
+ placeholder: "e.g., BSc Computer Science"
|
|
|
|
|
+ )
|
|
|
|
|
+ let institutionGroup = WorkExperienceEntryView.labeledFieldStack(
|
|
|
|
|
+ title: "Institution *",
|
|
|
|
|
+ field: institutionField,
|
|
|
|
|
+ placeholder: "e.g., MIT"
|
|
|
|
|
+ )
|
|
|
|
|
+ configureTwoColumnRow(degreeInstitutionRow, left: degreeGroup, right: institutionGroup)
|
|
|
|
|
+ pinEqualColumnWidths(in: degreeInstitutionRow)
|
|
|
|
|
+
|
|
|
|
|
+ let yearGroup = WorkExperienceEntryView.labeledFieldStack(
|
|
|
|
|
+ title: "Year *",
|
|
|
|
|
+ field: yearField,
|
|
|
|
|
+ placeholder: "e.g., 2020"
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ let inner = NSStackView(views: [headerRow, degreeInstitutionRow, yearGroup])
|
|
|
|
|
+ inner.orientation = .vertical
|
|
|
|
|
+ inner.spacing = 14
|
|
|
|
|
+ inner.alignment = .width
|
|
|
|
|
+ inner.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ inner.edgeInsets = NSEdgeInsets(top: 14, left: 18, bottom: 14, right: 18)
|
|
|
|
|
+ inner.userInterfaceLayoutDirection = .leftToRight
|
|
|
|
|
+ ProfileLayoutEnforcement.applyForcedLTR(to: inner)
|
|
|
|
|
+
|
|
|
|
|
+ addSubview(inner)
|
|
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
|
|
+ inner.leftAnchor.constraint(equalTo: leftAnchor),
|
|
|
|
|
+ inner.rightAnchor.constraint(equalTo: rightAnchor),
|
|
|
|
|
+ inner.topAnchor.constraint(equalTo: topAnchor),
|
|
|
|
|
+ inner.bottomAnchor.constraint(equalTo: bottomAnchor)
|
|
|
|
|
+ ])
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ 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() {
|
|
|
|
|
+ super.layout()
|
|
|
|
|
+ guard let layer = layer, layer.shadowOpacity > 0 else { return }
|
|
|
|
|
+ let r = layer.cornerRadius
|
|
|
|
|
+ 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() {
|
|
|
|
|
+ onDelete?()
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// MARK: - Primary CTA
|
|
// MARK: - Primary CTA
|
|
|
|
|
|
|
|
private final class ProfilePrimaryButton: NSButton {
|
|
private final class ProfilePrimaryButton: NSButton {
|