|
|
@@ -51,6 +51,8 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
51
|
51
|
private let findJobsCTAChrome = NSView()
|
|
52
|
52
|
private var findJobsCTAGradientLayer: CAGradientLayer?
|
|
53
|
53
|
private let scrollView = NSScrollView()
|
|
|
54
|
+ private let jobListingsContainer = NSView()
|
|
|
55
|
+ private let jobListingsStack = NSStackView()
|
|
54
|
56
|
|
|
55
|
57
|
private var currentSidebarItems: [SidebarItem] = []
|
|
56
|
58
|
private var selectedSidebarIndex: Int = 0
|
|
|
@@ -71,6 +73,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
71
|
73
|
updateSearchBarShadowPath()
|
|
72
|
74
|
findJobsCTAGradientLayer?.frame = findJobsCTAChrome.bounds
|
|
73
|
75
|
updateFindJobsCTAShadowPath()
|
|
|
76
|
+ updateJobListingDescriptionWidths()
|
|
74
|
77
|
}
|
|
75
|
78
|
|
|
76
|
79
|
func render(_ data: DashboardData) {
|
|
|
@@ -81,6 +84,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
81
|
84
|
selectedSidebarIndex = max(0, currentSidebarItems.count - 1)
|
|
82
|
85
|
}
|
|
83
|
86
|
configureSidebar()
|
|
|
87
|
+ configureJobListings(data.jobListings)
|
|
84
|
88
|
updateDocumentLayout()
|
|
85
|
89
|
}
|
|
86
|
90
|
|
|
|
@@ -163,6 +167,27 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
163
|
167
|
midSpacer.translatesAutoresizingMaskIntoConstraints = false
|
|
164
|
168
|
midSpacer.heightAnchor.constraint(equalToConstant: 20).isActive = true
|
|
165
|
169
|
|
|
|
170
|
+ let listingsTopSpacer = NSView()
|
|
|
171
|
+ listingsTopSpacer.translatesAutoresizingMaskIntoConstraints = false
|
|
|
172
|
+ listingsTopSpacer.heightAnchor.constraint(equalToConstant: 28).isActive = true
|
|
|
173
|
+
|
|
|
174
|
+ jobListingsContainer.translatesAutoresizingMaskIntoConstraints = false
|
|
|
175
|
+ jobListingsStack.orientation = .vertical
|
|
|
176
|
+ jobListingsStack.spacing = 14
|
|
|
177
|
+ // `.leading` keeps cards left-anchored; explicit width constraints below stretch each card across the full row.
|
|
|
178
|
+ jobListingsStack.alignment = .leading
|
|
|
179
|
+ jobListingsStack.distribution = .fill
|
|
|
180
|
+ jobListingsStack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
181
|
+ jobListingsStack.setContentHuggingPriority(.defaultHigh, for: .vertical)
|
|
|
182
|
+ jobListingsStack.setHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
183
|
+ jobListingsContainer.addSubview(jobListingsStack)
|
|
|
184
|
+ NSLayoutConstraint.activate([
|
|
|
185
|
+ jobListingsStack.leadingAnchor.constraint(equalTo: jobListingsContainer.leadingAnchor),
|
|
|
186
|
+ jobListingsStack.trailingAnchor.constraint(equalTo: jobListingsContainer.trailingAnchor),
|
|
|
187
|
+ jobListingsStack.topAnchor.constraint(equalTo: jobListingsContainer.topAnchor),
|
|
|
188
|
+ jobListingsStack.bottomAnchor.constraint(equalTo: jobListingsContainer.bottomAnchor)
|
|
|
189
|
+ ])
|
|
|
190
|
+
|
|
166
|
191
|
let overlayBottomSpacer = NSView()
|
|
167
|
192
|
overlayBottomSpacer.translatesAutoresizingMaskIntoConstraints = false
|
|
168
|
193
|
overlayBottomSpacer.setContentHuggingPriority(.defaultLow, for: .vertical)
|
|
|
@@ -172,6 +197,8 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
172
|
197
|
mainOverlay.addArrangedSubview(titleBlock)
|
|
173
|
198
|
mainOverlay.addArrangedSubview(midSpacer)
|
|
174
|
199
|
mainOverlay.addArrangedSubview(searchBarShadowHost)
|
|
|
200
|
+ mainOverlay.addArrangedSubview(listingsTopSpacer)
|
|
|
201
|
+ mainOverlay.addArrangedSubview(jobListingsContainer)
|
|
175
|
202
|
mainOverlay.addArrangedSubview(overlayBottomSpacer)
|
|
176
|
203
|
|
|
177
|
204
|
contentStack.addArrangedSubview(sidebar)
|
|
|
@@ -202,6 +229,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
202
|
229
|
mainOverlay.bottomAnchor.constraint(equalTo: mainHost.bottomAnchor, constant: -24),
|
|
203
|
230
|
|
|
204
|
231
|
searchBarShadowHost.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
|
|
|
232
|
+ jobListingsContainer.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
|
|
205
|
233
|
|
|
206
|
234
|
greetingLabel.leadingAnchor.constraint(equalTo: mainOverlay.leadingAnchor, constant: 24),
|
|
207
|
235
|
greetingLabel.trailingAnchor.constraint(equalTo: mainOverlay.trailingAnchor, constant: -24),
|
|
|
@@ -210,6 +238,84 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
210
|
238
|
])
|
|
211
|
239
|
}
|
|
212
|
240
|
|
|
|
241
|
+ private func updateJobListingDescriptionWidths() {
|
|
|
242
|
+ let containerWidth = jobListingsContainer.bounds.width
|
|
|
243
|
+ guard containerWidth > 1 else { return }
|
|
|
244
|
+ let innerWidth = containerWidth - 32
|
|
|
245
|
+ var didChange = false
|
|
|
246
|
+ for card in jobListingsStack.arrangedSubviews {
|
|
|
247
|
+ guard let desc = card.viewWithTag(502) as? NSTextField else { continue }
|
|
|
248
|
+ if abs(desc.preferredMaxLayoutWidth - innerWidth) > 0.5 {
|
|
|
249
|
+ desc.preferredMaxLayoutWidth = innerWidth
|
|
|
250
|
+ desc.invalidateIntrinsicContentSize()
|
|
|
251
|
+ didChange = true
|
|
|
252
|
+ }
|
|
|
253
|
+ }
|
|
|
254
|
+ if didChange {
|
|
|
255
|
+ // Wrapping width changed, so card heights need to recompute against the new intrinsic sizes.
|
|
|
256
|
+ jobListingsStack.needsLayout = true
|
|
|
257
|
+ }
|
|
|
258
|
+ }
|
|
|
259
|
+
|
|
|
260
|
+ private func configureJobListings(_ jobs: [JobListing]) {
|
|
|
261
|
+ jobListingsStack.arrangedSubviews.forEach {
|
|
|
262
|
+ jobListingsStack.removeArrangedSubview($0)
|
|
|
263
|
+ $0.removeFromSuperview()
|
|
|
264
|
+ }
|
|
|
265
|
+ for job in jobs {
|
|
|
266
|
+ let card = makeJobListingCard(job)
|
|
|
267
|
+ jobListingsStack.addArrangedSubview(card)
|
|
|
268
|
+ // Force every card to span the full row instead of hugging its intrinsic content width.
|
|
|
269
|
+ card.widthAnchor.constraint(equalTo: jobListingsStack.widthAnchor).isActive = true
|
|
|
270
|
+ }
|
|
|
271
|
+ }
|
|
|
272
|
+
|
|
|
273
|
+ private func makeJobListingCard(_ job: JobListing) -> NSView {
|
|
|
274
|
+ let card = NSView()
|
|
|
275
|
+ card.translatesAutoresizingMaskIntoConstraints = false
|
|
|
276
|
+ card.wantsLayer = true
|
|
|
277
|
+ card.layer?.backgroundColor = Theme.cardBackground.cgColor
|
|
|
278
|
+ card.layer?.cornerRadius = 12
|
|
|
279
|
+ card.layer?.borderWidth = 1
|
|
|
280
|
+ card.layer?.borderColor = Theme.border.cgColor
|
|
|
281
|
+ card.layer?.masksToBounds = true
|
|
|
282
|
+
|
|
|
283
|
+ let titleField = NSTextField(labelWithString: job.title)
|
|
|
284
|
+ titleField.font = .systemFont(ofSize: 16, weight: .semibold)
|
|
|
285
|
+ titleField.textColor = Theme.primaryText
|
|
|
286
|
+ titleField.maximumNumberOfLines = 2
|
|
|
287
|
+ titleField.lineBreakMode = .byWordWrapping
|
|
|
288
|
+ titleField.translatesAutoresizingMaskIntoConstraints = false
|
|
|
289
|
+
|
|
|
290
|
+ let descriptionField = NSTextField(wrappingLabelWithString: job.description)
|
|
|
291
|
+ descriptionField.font = .systemFont(ofSize: 13, weight: .regular)
|
|
|
292
|
+ descriptionField.textColor = Theme.secondaryText
|
|
|
293
|
+ descriptionField.maximumNumberOfLines = 0
|
|
|
294
|
+ descriptionField.tag = 502
|
|
|
295
|
+ descriptionField.translatesAutoresizingMaskIntoConstraints = false
|
|
|
296
|
+
|
|
|
297
|
+ let inner = NSStackView(views: [titleField, descriptionField])
|
|
|
298
|
+ inner.orientation = .vertical
|
|
|
299
|
+ inner.spacing = 6
|
|
|
300
|
+ inner.alignment = .leading
|
|
|
301
|
+ inner.translatesAutoresizingMaskIntoConstraints = false
|
|
|
302
|
+
|
|
|
303
|
+ card.addSubview(inner)
|
|
|
304
|
+ NSLayoutConstraint.activate([
|
|
|
305
|
+ inner.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 16),
|
|
|
306
|
+ inner.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -16),
|
|
|
307
|
+ inner.topAnchor.constraint(equalTo: card.topAnchor, constant: 14),
|
|
|
308
|
+ inner.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -14),
|
|
|
309
|
+
|
|
|
310
|
+ titleField.leadingAnchor.constraint(equalTo: inner.leadingAnchor),
|
|
|
311
|
+ titleField.trailingAnchor.constraint(equalTo: inner.trailingAnchor),
|
|
|
312
|
+ descriptionField.leadingAnchor.constraint(equalTo: inner.leadingAnchor),
|
|
|
313
|
+ descriptionField.trailingAnchor.constraint(equalTo: inner.trailingAnchor)
|
|
|
314
|
+ ])
|
|
|
315
|
+
|
|
|
316
|
+ return card
|
|
|
317
|
+ }
|
|
|
318
|
+
|
|
213
|
319
|
private func configureSearchBar() {
|
|
214
|
320
|
let pillCorner: CGFloat = 27
|
|
215
|
321
|
let barHeight: CGFloat = 54
|