|
|
@@ -6,6 +6,11 @@
|
|
|
import Cocoa
|
|
|
import QuartzCore
|
|
|
|
|
|
+private enum JobListingCardContext {
|
|
|
+ case homeSearchResults
|
|
|
+ case savedJobsPage
|
|
|
+}
|
|
|
+
|
|
|
final class DashboardView: NSView, NSTextFieldDelegate {
|
|
|
/// Indeed.com-inspired neutrals and brand blue (white surfaces, `#2557a7` accent, `#2d2d2d` / `#767676` text, `#d4d2d0` borders).
|
|
|
private enum Theme {
|
|
|
@@ -55,8 +60,15 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
|
private let jobListingsStack = NSStackView()
|
|
|
/// Shown when a sidebar item other than Home is selected.
|
|
|
private let nonHomeHost = NSView()
|
|
|
+ private let nonHomeGenericContainer = NSView()
|
|
|
private let nonHomeTitleLabel = NSTextField(labelWithString: "")
|
|
|
private let nonHomeSubtitleLabel = NSTextField(wrappingLabelWithString: "")
|
|
|
+ private let savedJobsPageContainer = NSView()
|
|
|
+ private let savedJobsPageTitleLabel = NSTextField(labelWithString: "Saved Jobs")
|
|
|
+ private let savedJobsPageSubtitleLabel = NSTextField(wrappingLabelWithString: "")
|
|
|
+ private let savedJobsScrollView = NSScrollView()
|
|
|
+ private let savedJobsDocumentView = JobListingsDocumentView()
|
|
|
+ private let savedJobsStack = NSStackView()
|
|
|
|
|
|
private var currentSidebarItems: [SidebarItem] = []
|
|
|
private var selectedSidebarIndex: Int = 0
|
|
|
@@ -65,7 +77,8 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
|
/// Last successful search result set (used when removing a card with the dismiss control).
|
|
|
private var lastSearchResults: [JobListing] = []
|
|
|
private var lastNoResultsQuery: String?
|
|
|
- private var savedJobs: Set<JobListing> = []
|
|
|
+ /// Most recently saved jobs appear first; persisted across launches.
|
|
|
+ private var savedJobOrder: [JobListing] = []
|
|
|
|
|
|
override init(frame frameRect: NSRect) {
|
|
|
super.init(frame: frameRect)
|
|
|
@@ -94,7 +107,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
|
}
|
|
|
configureSidebar()
|
|
|
catalogJobListings = data.jobListings
|
|
|
- savedJobs = []
|
|
|
+ savedJobOrder = Self.normalizedSavedJobs(SavedJobsStore.load())
|
|
|
configureJobListings([], noResultsForQuery: nil)
|
|
|
updateMainContentVisibility()
|
|
|
}
|
|
|
@@ -251,12 +264,16 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
|
}
|
|
|
|
|
|
private func updateJobListingDescriptionWidths() {
|
|
|
- let containerWidth = jobListingsContainer.bounds.width
|
|
|
+ updateDescriptionColumnWidths(in: jobListingsStack, containerWidth: jobListingsContainer.bounds.width)
|
|
|
+ updateDescriptionColumnWidths(in: savedJobsStack, containerWidth: savedJobsDocumentView.bounds.width)
|
|
|
+ }
|
|
|
+
|
|
|
+ private func updateDescriptionColumnWidths(in stack: NSStackView, containerWidth: CGFloat) {
|
|
|
guard containerWidth > 1 else { return }
|
|
|
let buttonStripReserve: CGFloat = 200
|
|
|
let fallbackTextColumn = max(1, containerWidth - 32 - buttonStripReserve)
|
|
|
var didChange = false
|
|
|
- for card in jobListingsStack.arrangedSubviews {
|
|
|
+ for card in stack.arrangedSubviews {
|
|
|
guard let desc = card.viewWithTag(502) as? NSTextField else { continue }
|
|
|
let columnWidth: CGFloat
|
|
|
if let column = desc.superview, column.bounds.width > 1 {
|
|
|
@@ -271,8 +288,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
|
}
|
|
|
}
|
|
|
if didChange {
|
|
|
- // Wrapping width changed, so card heights need to recompute against the new intrinsic sizes.
|
|
|
- jobListingsStack.needsLayout = true
|
|
|
+ stack.needsLayout = true
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -297,14 +313,41 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
|
return
|
|
|
}
|
|
|
for job in jobs {
|
|
|
- let card = makeJobListingCard(job)
|
|
|
+ let card = makeJobListingCard(job, context: .homeSearchResults)
|
|
|
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 {
|
|
|
+ private static func normalizedSavedJobs(_ jobs: [JobListing]) -> [JobListing] {
|
|
|
+ var seen = Set<JobListing>()
|
|
|
+ var out: [JobListing] = []
|
|
|
+ for job in jobs where seen.insert(job).inserted {
|
|
|
+ out.append(job)
|
|
|
+ }
|
|
|
+ return out
|
|
|
+ }
|
|
|
+
|
|
|
+ private func isJobSaved(_ job: JobListing) -> Bool {
|
|
|
+ savedJobOrder.contains(job)
|
|
|
+ }
|
|
|
+
|
|
|
+ private func persistSavedJobs() {
|
|
|
+ SavedJobsStore.save(savedJobOrder)
|
|
|
+ }
|
|
|
+
|
|
|
+ private func applySavedState(_ saved: Bool, for job: JobListing) {
|
|
|
+ if saved {
|
|
|
+ savedJobOrder.removeAll { $0 == job }
|
|
|
+ savedJobOrder.insert(job, at: 0)
|
|
|
+ } else {
|
|
|
+ savedJobOrder.removeAll { $0 == job }
|
|
|
+ }
|
|
|
+ persistSavedJobs()
|
|
|
+ }
|
|
|
+
|
|
|
+ private func makeJobListingCard(_ job: JobListing, context: JobListingCardContext) -> NSView {
|
|
|
let card = NSView()
|
|
|
card.translatesAutoresizingMaskIntoConstraints = false
|
|
|
card.wantsLayer = true
|
|
|
@@ -331,6 +374,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
|
|
|
|
let applyButton = JobPayloadButton(title: "Apply", target: self, action: #selector(didTapJobApply(_:)))
|
|
|
applyButton.jobPayload = job
|
|
|
+ applyButton.cardContext = context
|
|
|
applyButton.isBordered = false
|
|
|
applyButton.bezelStyle = .rounded
|
|
|
applyButton.font = .systemFont(ofSize: 13, weight: .semibold)
|
|
|
@@ -342,20 +386,23 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
|
applyButton.setContentHuggingPriority(.required, for: .horizontal)
|
|
|
applyButton.setContentCompressionResistancePriority(.required, for: .horizontal)
|
|
|
|
|
|
- let savedButton = JobPayloadButton(title: "Saved", target: self, action: #selector(didTapJobSaved(_:)))
|
|
|
+ let savedOn = isJobSaved(job)
|
|
|
+ let savedButton = JobPayloadButton(title: savedOn ? "Saved" : "Save", target: self, action: #selector(didTapJobSaved(_:)))
|
|
|
savedButton.jobPayload = job
|
|
|
+ savedButton.cardContext = context
|
|
|
savedButton.setButtonType(.toggle)
|
|
|
savedButton.isBordered = false
|
|
|
savedButton.bezelStyle = .rounded
|
|
|
savedButton.font = .systemFont(ofSize: 13, weight: .semibold)
|
|
|
savedButton.focusRingType = .none
|
|
|
- savedButton.state = savedJobs.contains(job) ? .on : .off
|
|
|
+ savedButton.state = savedOn ? .on : .off
|
|
|
styleJobSavedButton(savedButton)
|
|
|
savedButton.setContentHuggingPriority(.required, for: .horizontal)
|
|
|
savedButton.setContentCompressionResistancePriority(.required, for: .horizontal)
|
|
|
|
|
|
let dismissButton = JobPayloadButton()
|
|
|
dismissButton.jobPayload = job
|
|
|
+ dismissButton.cardContext = context
|
|
|
dismissButton.image = NSImage(systemSymbolName: "xmark", accessibilityDescription: "Dismiss")
|
|
|
dismissButton.imagePosition = .imageOnly
|
|
|
dismissButton.imageScaling = .scaleProportionallyDown
|
|
|
@@ -365,7 +412,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
|
dismissButton.contentTintColor = Theme.secondaryText
|
|
|
dismissButton.target = self
|
|
|
dismissButton.action = #selector(didTapJobDismiss(_:))
|
|
|
- dismissButton.toolTip = "Dismiss"
|
|
|
+ dismissButton.toolTip = context == .savedJobsPage ? "Remove from saved" : "Dismiss"
|
|
|
dismissButton.focusRingType = .none
|
|
|
dismissButton.setContentHuggingPriority(.required, for: .horizontal)
|
|
|
|
|
|
@@ -446,19 +493,29 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
|
|
|
|
@objc private func didTapJobSaved(_ sender: NSButton) {
|
|
|
guard let job = (sender as? JobPayloadButton)?.jobPayload else { return }
|
|
|
- if savedJobs.contains(job) {
|
|
|
- savedJobs.remove(job)
|
|
|
- } else {
|
|
|
- savedJobs.insert(job)
|
|
|
- }
|
|
|
- sender.state = savedJobs.contains(job) ? .on : .off
|
|
|
+ let willSave = !isJobSaved(job)
|
|
|
+ applySavedState(willSave, for: job)
|
|
|
+ sender.state = willSave ? .on : .off
|
|
|
+ sender.title = willSave ? "Saved" : "Save"
|
|
|
styleJobSavedButton(sender)
|
|
|
+ if isSavedJobsSidebarIndex(selectedSidebarIndex) {
|
|
|
+ reloadSavedJobsListings()
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
@objc private func didTapJobDismiss(_ sender: NSButton) {
|
|
|
- guard let job = (sender as? JobPayloadButton)?.jobPayload else { return }
|
|
|
- lastSearchResults.removeAll { $0 == job }
|
|
|
- configureJobListings(lastSearchResults, noResultsForQuery: lastNoResultsQuery)
|
|
|
+ guard let button = sender as? JobPayloadButton, let job = button.jobPayload else { return }
|
|
|
+ switch button.cardContext {
|
|
|
+ case .homeSearchResults:
|
|
|
+ lastSearchResults.removeAll { $0 == job }
|
|
|
+ configureJobListings(lastSearchResults, noResultsForQuery: lastNoResultsQuery)
|
|
|
+ case .savedJobsPage:
|
|
|
+ applySavedState(false, for: job)
|
|
|
+ reloadSavedJobsListings()
|
|
|
+ if isHomeSidebarIndex(selectedSidebarIndex), !lastSearchResults.isEmpty {
|
|
|
+ configureJobListings(lastSearchResults, noResultsForQuery: lastNoResultsQuery, updateLastResults: false)
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
private func configureSearchBar() {
|
|
|
@@ -630,6 +687,23 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
|
nonHomeHost.layer?.backgroundColor = Theme.mainHostBackground.cgColor
|
|
|
nonHomeHost.isHidden = true
|
|
|
|
|
|
+ nonHomeGenericContainer.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ savedJobsPageContainer.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ nonHomeHost.addSubview(nonHomeGenericContainer)
|
|
|
+ nonHomeHost.addSubview(savedJobsPageContainer)
|
|
|
+
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
+ nonHomeGenericContainer.leadingAnchor.constraint(equalTo: nonHomeHost.leadingAnchor),
|
|
|
+ nonHomeGenericContainer.trailingAnchor.constraint(equalTo: nonHomeHost.trailingAnchor),
|
|
|
+ nonHomeGenericContainer.topAnchor.constraint(equalTo: nonHomeHost.topAnchor),
|
|
|
+ nonHomeGenericContainer.bottomAnchor.constraint(equalTo: nonHomeHost.bottomAnchor),
|
|
|
+
|
|
|
+ savedJobsPageContainer.leadingAnchor.constraint(equalTo: nonHomeHost.leadingAnchor),
|
|
|
+ savedJobsPageContainer.trailingAnchor.constraint(equalTo: nonHomeHost.trailingAnchor),
|
|
|
+ savedJobsPageContainer.topAnchor.constraint(equalTo: nonHomeHost.topAnchor),
|
|
|
+ savedJobsPageContainer.bottomAnchor.constraint(equalTo: nonHomeHost.bottomAnchor)
|
|
|
+ ])
|
|
|
+
|
|
|
nonHomeTitleLabel.font = .systemFont(ofSize: 22, weight: .bold)
|
|
|
nonHomeTitleLabel.textColor = Theme.primaryText
|
|
|
nonHomeTitleLabel.alignment = .center
|
|
|
@@ -641,19 +715,108 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
|
nonHomeSubtitleLabel.maximumNumberOfLines = 0
|
|
|
nonHomeSubtitleLabel.stringValue = "This area is not available in the preview build. Use Home to search jobs."
|
|
|
|
|
|
- let stack = NSStackView(views: [nonHomeTitleLabel, nonHomeSubtitleLabel])
|
|
|
- stack.orientation = .vertical
|
|
|
- stack.spacing = 10
|
|
|
- stack.alignment = .centerX
|
|
|
- stack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
- nonHomeHost.addSubview(stack)
|
|
|
+ let genericStack = NSStackView(views: [nonHomeTitleLabel, nonHomeSubtitleLabel])
|
|
|
+ genericStack.orientation = .vertical
|
|
|
+ genericStack.spacing = 10
|
|
|
+ genericStack.alignment = .centerX
|
|
|
+ genericStack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ nonHomeGenericContainer.addSubview(genericStack)
|
|
|
NSLayoutConstraint.activate([
|
|
|
- stack.centerXAnchor.constraint(equalTo: nonHomeHost.centerXAnchor),
|
|
|
- stack.centerYAnchor.constraint(equalTo: nonHomeHost.centerYAnchor),
|
|
|
- stack.leadingAnchor.constraint(greaterThanOrEqualTo: nonHomeHost.leadingAnchor, constant: 32),
|
|
|
- stack.trailingAnchor.constraint(lessThanOrEqualTo: nonHomeHost.trailingAnchor, constant: -32),
|
|
|
+ genericStack.centerXAnchor.constraint(equalTo: nonHomeGenericContainer.centerXAnchor),
|
|
|
+ genericStack.centerYAnchor.constraint(equalTo: nonHomeGenericContainer.centerYAnchor),
|
|
|
+ genericStack.leadingAnchor.constraint(greaterThanOrEqualTo: nonHomeGenericContainer.leadingAnchor, constant: 32),
|
|
|
+ genericStack.trailingAnchor.constraint(lessThanOrEqualTo: nonHomeGenericContainer.trailingAnchor, constant: -32),
|
|
|
nonHomeSubtitleLabel.widthAnchor.constraint(lessThanOrEqualToConstant: 420)
|
|
|
])
|
|
|
+
|
|
|
+ savedJobsPageTitleLabel.font = .systemFont(ofSize: 22, weight: .bold)
|
|
|
+ savedJobsPageTitleLabel.textColor = Theme.primaryText
|
|
|
+ savedJobsPageTitleLabel.alignment = .left
|
|
|
+ savedJobsPageTitleLabel.maximumNumberOfLines = 1
|
|
|
+
|
|
|
+ savedJobsPageSubtitleLabel.font = .systemFont(ofSize: 14, weight: .regular)
|
|
|
+ savedJobsPageSubtitleLabel.textColor = Theme.secondaryText
|
|
|
+ savedJobsPageSubtitleLabel.alignment = .left
|
|
|
+ savedJobsPageSubtitleLabel.maximumNumberOfLines = 0
|
|
|
+
|
|
|
+ savedJobsDocumentView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ savedJobsStack.orientation = .vertical
|
|
|
+ savedJobsStack.spacing = 14
|
|
|
+ savedJobsStack.alignment = .leading
|
|
|
+ savedJobsStack.distribution = .fill
|
|
|
+ savedJobsStack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ savedJobsStack.setContentHuggingPriority(.defaultHigh, for: .vertical)
|
|
|
+ savedJobsStack.setHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
+ savedJobsDocumentView.addSubview(savedJobsStack)
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
+ savedJobsStack.leadingAnchor.constraint(equalTo: savedJobsDocumentView.leadingAnchor),
|
|
|
+ savedJobsStack.trailingAnchor.constraint(equalTo: savedJobsDocumentView.trailingAnchor),
|
|
|
+ savedJobsStack.topAnchor.constraint(equalTo: savedJobsDocumentView.topAnchor),
|
|
|
+ savedJobsStack.bottomAnchor.constraint(equalTo: savedJobsDocumentView.bottomAnchor)
|
|
|
+ ])
|
|
|
+
|
|
|
+ savedJobsScrollView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ savedJobsScrollView.hasVerticalScroller = true
|
|
|
+ savedJobsScrollView.hasHorizontalScroller = false
|
|
|
+ savedJobsScrollView.autohidesScrollers = true
|
|
|
+ savedJobsScrollView.drawsBackground = false
|
|
|
+ savedJobsScrollView.borderType = .noBorder
|
|
|
+ savedJobsScrollView.documentView = savedJobsDocumentView
|
|
|
+ savedJobsScrollView.setContentHuggingPriority(.defaultLow, for: .vertical)
|
|
|
+ savedJobsScrollView.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
|
|
|
+
|
|
|
+ let savedHeaderStack = NSStackView(views: [savedJobsPageTitleLabel, savedJobsPageSubtitleLabel])
|
|
|
+ savedHeaderStack.orientation = .vertical
|
|
|
+ savedHeaderStack.spacing = 6
|
|
|
+ savedHeaderStack.alignment = .leading
|
|
|
+ savedHeaderStack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+
|
|
|
+ let savedOuterStack = NSStackView(views: [savedHeaderStack, savedJobsScrollView])
|
|
|
+ savedOuterStack.orientation = .vertical
|
|
|
+ savedOuterStack.spacing = 16
|
|
|
+ savedOuterStack.alignment = .width
|
|
|
+ savedOuterStack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ savedJobsPageContainer.addSubview(savedOuterStack)
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
+ savedOuterStack.leadingAnchor.constraint(equalTo: savedJobsPageContainer.leadingAnchor, constant: 32),
|
|
|
+ savedOuterStack.trailingAnchor.constraint(equalTo: savedJobsPageContainer.trailingAnchor, constant: -32),
|
|
|
+ savedOuterStack.topAnchor.constraint(equalTo: savedJobsPageContainer.topAnchor, constant: 8),
|
|
|
+ savedOuterStack.bottomAnchor.constraint(equalTo: savedJobsPageContainer.bottomAnchor),
|
|
|
+
|
|
|
+ savedJobsDocumentView.topAnchor.constraint(equalTo: savedJobsScrollView.contentView.topAnchor),
|
|
|
+ savedJobsDocumentView.leadingAnchor.constraint(equalTo: savedJobsScrollView.contentView.leadingAnchor),
|
|
|
+ savedJobsDocumentView.widthAnchor.constraint(equalTo: savedJobsScrollView.contentView.widthAnchor)
|
|
|
+ ])
|
|
|
+ }
|
|
|
+
|
|
|
+ private func reloadSavedJobsListings() {
|
|
|
+ savedJobsStack.arrangedSubviews.forEach {
|
|
|
+ savedJobsStack.removeArrangedSubview($0)
|
|
|
+ $0.removeFromSuperview()
|
|
|
+ }
|
|
|
+ if savedJobOrder.isEmpty {
|
|
|
+ savedJobsPageSubtitleLabel.stringValue = "Save jobs from Home to see them here."
|
|
|
+ let empty = NSTextField(wrappingLabelWithString: "No saved jobs yet. Search on Home, then tap Save on a listing.")
|
|
|
+ empty.font = .systemFont(ofSize: 14, weight: .regular)
|
|
|
+ empty.textColor = Theme.secondaryText
|
|
|
+ empty.alignment = .left
|
|
|
+ empty.maximumNumberOfLines = 0
|
|
|
+ empty.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ savedJobsStack.addArrangedSubview(empty)
|
|
|
+ empty.widthAnchor.constraint(equalTo: savedJobsStack.widthAnchor).isActive = true
|
|
|
+ return
|
|
|
+ }
|
|
|
+ savedJobsPageSubtitleLabel.stringValue = "\(savedJobOrder.count) saved \(savedJobOrder.count == 1 ? "position" : "positions")"
|
|
|
+ for job in savedJobOrder {
|
|
|
+ let card = makeJobListingCard(job, context: .savedJobsPage)
|
|
|
+ savedJobsStack.addArrangedSubview(card)
|
|
|
+ card.widthAnchor.constraint(equalTo: savedJobsStack.widthAnchor).isActive = true
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func isSavedJobsSidebarIndex(_ index: Int) -> Bool {
|
|
|
+ guard index >= 0, index < currentSidebarItems.count else { return false }
|
|
|
+ return currentSidebarItems[index].title == "Saved Jobs"
|
|
|
}
|
|
|
|
|
|
private func isHomeSidebarIndex(_ index: Int) -> Bool {
|
|
|
@@ -663,10 +826,17 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
|
|
|
|
private func updateMainContentVisibility() {
|
|
|
let home = isHomeSidebarIndex(selectedSidebarIndex)
|
|
|
+ let savedJobs = isSavedJobsSidebarIndex(selectedSidebarIndex)
|
|
|
mainOverlay.isHidden = !home
|
|
|
nonHomeHost.isHidden = home
|
|
|
+ nonHomeGenericContainer.isHidden = savedJobs
|
|
|
+ savedJobsPageContainer.isHidden = !savedJobs
|
|
|
if !home, selectedSidebarIndex < currentSidebarItems.count {
|
|
|
- nonHomeTitleLabel.stringValue = currentSidebarItems[selectedSidebarIndex].title
|
|
|
+ if savedJobs {
|
|
|
+ reloadSavedJobsListings()
|
|
|
+ } else {
|
|
|
+ nonHomeTitleLabel.stringValue = currentSidebarItems[selectedSidebarIndex].title
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -950,6 +1120,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
|
/// `NSButton` that carries a `JobListing` for card actions (`representedObject` is unavailable on `NSButton` in this target).
|
|
|
private final class JobPayloadButton: NSButton {
|
|
|
var jobPayload: JobListing?
|
|
|
+ var cardContext: JobListingCardContext = .homeSearchResults
|
|
|
}
|
|
|
|
|
|
/// Document view for the job list `NSScrollView`; flipped coordinates keep short result sets aligned to the top of the clip (avoids a large empty band above the cards on macOS).
|