|
|
@@ -51,6 +51,8 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
|
private let findJobsCTAChrome = NSView()
|
|
|
private var findJobsCTAGradientLayer: CAGradientLayer?
|
|
|
private let scrollView = NSScrollView()
|
|
|
+ private let jobListingsContainer = NSView()
|
|
|
+ private let jobListingsStack = NSStackView()
|
|
|
|
|
|
private var currentSidebarItems: [SidebarItem] = []
|
|
|
private var selectedSidebarIndex: Int = 0
|
|
|
@@ -71,6 +73,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
|
updateSearchBarShadowPath()
|
|
|
findJobsCTAGradientLayer?.frame = findJobsCTAChrome.bounds
|
|
|
updateFindJobsCTAShadowPath()
|
|
|
+ updateJobListingDescriptionWidths()
|
|
|
}
|
|
|
|
|
|
func render(_ data: DashboardData) {
|
|
|
@@ -81,6 +84,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
|
selectedSidebarIndex = max(0, currentSidebarItems.count - 1)
|
|
|
}
|
|
|
configureSidebar()
|
|
|
+ configureJobListings(data.jobListings)
|
|
|
updateDocumentLayout()
|
|
|
}
|
|
|
|
|
|
@@ -163,6 +167,27 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
|
midSpacer.translatesAutoresizingMaskIntoConstraints = false
|
|
|
midSpacer.heightAnchor.constraint(equalToConstant: 20).isActive = true
|
|
|
|
|
|
+ let listingsTopSpacer = NSView()
|
|
|
+ listingsTopSpacer.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ listingsTopSpacer.heightAnchor.constraint(equalToConstant: 28).isActive = true
|
|
|
+
|
|
|
+ jobListingsContainer.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ jobListingsStack.orientation = .vertical
|
|
|
+ jobListingsStack.spacing = 14
|
|
|
+ // `.leading` keeps cards left-anchored; explicit width constraints below stretch each card across the full row.
|
|
|
+ jobListingsStack.alignment = .leading
|
|
|
+ jobListingsStack.distribution = .fill
|
|
|
+ jobListingsStack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ jobListingsStack.setContentHuggingPriority(.defaultHigh, for: .vertical)
|
|
|
+ jobListingsStack.setHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
+ jobListingsContainer.addSubview(jobListingsStack)
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
+ jobListingsStack.leadingAnchor.constraint(equalTo: jobListingsContainer.leadingAnchor),
|
|
|
+ jobListingsStack.trailingAnchor.constraint(equalTo: jobListingsContainer.trailingAnchor),
|
|
|
+ jobListingsStack.topAnchor.constraint(equalTo: jobListingsContainer.topAnchor),
|
|
|
+ jobListingsStack.bottomAnchor.constraint(equalTo: jobListingsContainer.bottomAnchor)
|
|
|
+ ])
|
|
|
+
|
|
|
let overlayBottomSpacer = NSView()
|
|
|
overlayBottomSpacer.translatesAutoresizingMaskIntoConstraints = false
|
|
|
overlayBottomSpacer.setContentHuggingPriority(.defaultLow, for: .vertical)
|
|
|
@@ -172,6 +197,8 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
|
mainOverlay.addArrangedSubview(titleBlock)
|
|
|
mainOverlay.addArrangedSubview(midSpacer)
|
|
|
mainOverlay.addArrangedSubview(searchBarShadowHost)
|
|
|
+ mainOverlay.addArrangedSubview(listingsTopSpacer)
|
|
|
+ mainOverlay.addArrangedSubview(jobListingsContainer)
|
|
|
mainOverlay.addArrangedSubview(overlayBottomSpacer)
|
|
|
|
|
|
contentStack.addArrangedSubview(sidebar)
|
|
|
@@ -202,6 +229,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
|
mainOverlay.bottomAnchor.constraint(equalTo: mainHost.bottomAnchor, constant: -24),
|
|
|
|
|
|
searchBarShadowHost.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
|
|
|
+ jobListingsContainer.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
|
|
|
|
|
|
greetingLabel.leadingAnchor.constraint(equalTo: mainOverlay.leadingAnchor, constant: 24),
|
|
|
greetingLabel.trailingAnchor.constraint(equalTo: mainOverlay.trailingAnchor, constant: -24),
|
|
|
@@ -210,6 +238,84 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
|
])
|
|
|
}
|
|
|
|
|
|
+ private func updateJobListingDescriptionWidths() {
|
|
|
+ let containerWidth = jobListingsContainer.bounds.width
|
|
|
+ guard containerWidth > 1 else { return }
|
|
|
+ let innerWidth = containerWidth - 32
|
|
|
+ var didChange = false
|
|
|
+ for card in jobListingsStack.arrangedSubviews {
|
|
|
+ guard let desc = card.viewWithTag(502) as? NSTextField else { continue }
|
|
|
+ if abs(desc.preferredMaxLayoutWidth - innerWidth) > 0.5 {
|
|
|
+ desc.preferredMaxLayoutWidth = innerWidth
|
|
|
+ desc.invalidateIntrinsicContentSize()
|
|
|
+ didChange = true
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if didChange {
|
|
|
+ // Wrapping width changed, so card heights need to recompute against the new intrinsic sizes.
|
|
|
+ jobListingsStack.needsLayout = true
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func configureJobListings(_ jobs: [JobListing]) {
|
|
|
+ jobListingsStack.arrangedSubviews.forEach {
|
|
|
+ jobListingsStack.removeArrangedSubview($0)
|
|
|
+ $0.removeFromSuperview()
|
|
|
+ }
|
|
|
+ for job in jobs {
|
|
|
+ let card = makeJobListingCard(job)
|
|
|
+ jobListingsStack.addArrangedSubview(card)
|
|
|
+ // Force every card to span the full row instead of hugging its intrinsic content width.
|
|
|
+ card.widthAnchor.constraint(equalTo: jobListingsStack.widthAnchor).isActive = true
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func makeJobListingCard(_ job: JobListing) -> NSView {
|
|
|
+ let card = NSView()
|
|
|
+ card.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ card.wantsLayer = true
|
|
|
+ card.layer?.backgroundColor = Theme.cardBackground.cgColor
|
|
|
+ card.layer?.cornerRadius = 12
|
|
|
+ card.layer?.borderWidth = 1
|
|
|
+ card.layer?.borderColor = Theme.border.cgColor
|
|
|
+ card.layer?.masksToBounds = true
|
|
|
+
|
|
|
+ let titleField = NSTextField(labelWithString: job.title)
|
|
|
+ titleField.font = .systemFont(ofSize: 16, weight: .semibold)
|
|
|
+ titleField.textColor = Theme.primaryText
|
|
|
+ titleField.maximumNumberOfLines = 2
|
|
|
+ titleField.lineBreakMode = .byWordWrapping
|
|
|
+ titleField.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+
|
|
|
+ let descriptionField = NSTextField(wrappingLabelWithString: job.description)
|
|
|
+ descriptionField.font = .systemFont(ofSize: 13, weight: .regular)
|
|
|
+ descriptionField.textColor = Theme.secondaryText
|
|
|
+ descriptionField.maximumNumberOfLines = 0
|
|
|
+ descriptionField.tag = 502
|
|
|
+ descriptionField.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+
|
|
|
+ let inner = NSStackView(views: [titleField, descriptionField])
|
|
|
+ inner.orientation = .vertical
|
|
|
+ inner.spacing = 6
|
|
|
+ inner.alignment = .leading
|
|
|
+ inner.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+
|
|
|
+ card.addSubview(inner)
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
+ inner.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 16),
|
|
|
+ inner.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -16),
|
|
|
+ inner.topAnchor.constraint(equalTo: card.topAnchor, constant: 14),
|
|
|
+ inner.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -14),
|
|
|
+
|
|
|
+ titleField.leadingAnchor.constraint(equalTo: inner.leadingAnchor),
|
|
|
+ titleField.trailingAnchor.constraint(equalTo: inner.trailingAnchor),
|
|
|
+ descriptionField.leadingAnchor.constraint(equalTo: inner.leadingAnchor),
|
|
|
+ descriptionField.trailingAnchor.constraint(equalTo: inner.trailingAnchor)
|
|
|
+ ])
|
|
|
+
|
|
|
+ return card
|
|
|
+ }
|
|
|
+
|
|
|
private func configureSearchBar() {
|
|
|
let pillCorner: CGFloat = 27
|
|
|
let barHeight: CGFloat = 54
|