Просмотр исходного кода

Improve job cards: top-right actions and full-width rows

- Lay out each card with title and Apply/Saved/dismiss on the top row and description full width below.
- Track last search results for dismiss refresh; add Apply (Indeed search), Saved toggle, and dismiss.
- Make JobListing Hashable for saved-job set storage; refine description preferred width from stack bounds.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 недель назад: 3
Родитель
Сommit
729b8ee003
2 измененных файлов с 144 добавлено и 21 удалено
  1. 1 1
      App for Indeed/Models/DashboardModels.swift
  2. 143 20
      App for Indeed/Views/DashboardView.swift

+ 1 - 1
App for Indeed/Models/DashboardModels.swift

@@ -11,7 +11,7 @@ struct SidebarItem {
11 11
     let badge: String?
12 12
 }
13 13
 
14
-struct JobListing {
14
+struct JobListing: Hashable {
15 15
     let title: String
16 16
     let description: String
17 17
 }

+ 143 - 20
App for Indeed/Views/DashboardView.swift

@@ -62,6 +62,10 @@ final class DashboardView: NSView, NSTextFieldDelegate {
62 62
     private var selectedSidebarIndex: Int = 0
63 63
     /// Full list from `DashboardData`; results are shown after the user runs a search.
64 64
     private var catalogJobListings: [JobListing] = []
65
+    /// Last successful search result set (used when removing a card with the dismiss control).
66
+    private var lastSearchResults: [JobListing] = []
67
+    private var lastNoResultsQuery: String?
68
+    private var savedJobs: Set<JobListing> = []
65 69
 
66 70
     override init(frame frameRect: NSRect) {
67 71
         super.init(frame: frameRect)
@@ -90,6 +94,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
90 94
         }
91 95
         configureSidebar()
92 96
         catalogJobListings = data.jobListings
97
+        savedJobs = []
93 98
         configureJobListings([], noResultsForQuery: nil)
94 99
         updateMainContentVisibility()
95 100
     }
@@ -248,12 +253,19 @@ final class DashboardView: NSView, NSTextFieldDelegate {
248 253
     private func updateJobListingDescriptionWidths() {
249 254
         let containerWidth = jobListingsContainer.bounds.width
250 255
         guard containerWidth > 1 else { return }
251
-        let innerWidth = containerWidth - 32
256
+        let buttonStripReserve: CGFloat = 200
257
+        let fallbackTextColumn = max(1, containerWidth - 32 - buttonStripReserve)
252 258
         var didChange = false
253 259
         for card in jobListingsStack.arrangedSubviews {
254 260
             guard let desc = card.viewWithTag(502) as? NSTextField else { continue }
255
-            if abs(desc.preferredMaxLayoutWidth - innerWidth) > 0.5 {
256
-                desc.preferredMaxLayoutWidth = innerWidth
261
+            let columnWidth: CGFloat
262
+            if let column = desc.superview, column.bounds.width > 1 {
263
+                columnWidth = column.bounds.width
264
+            } else {
265
+                columnWidth = fallbackTextColumn
266
+            }
267
+            if abs(desc.preferredMaxLayoutWidth - columnWidth) > 0.5 {
268
+                desc.preferredMaxLayoutWidth = columnWidth
257 269
                 desc.invalidateIntrinsicContentSize()
258 270
                 didChange = true
259 271
             }
@@ -264,7 +276,11 @@ final class DashboardView: NSView, NSTextFieldDelegate {
264 276
         }
265 277
     }
266 278
 
267
-    private func configureJobListings(_ jobs: [JobListing], noResultsForQuery: String?) {
279
+    private func configureJobListings(_ jobs: [JobListing], noResultsForQuery: String?, updateLastResults: Bool = true) {
280
+        if updateLastResults {
281
+            lastSearchResults = jobs
282
+            lastNoResultsQuery = noResultsForQuery
283
+        }
268 284
         jobListingsStack.arrangedSubviews.forEach {
269 285
             jobListingsStack.removeArrangedSubview($0)
270 286
             $0.removeFromSuperview()
@@ -312,28 +328,130 @@ final class DashboardView: NSView, NSTextFieldDelegate {
312 328
         descriptionField.tag = 502
313 329
         descriptionField.translatesAutoresizingMaskIntoConstraints = false
314 330
 
315
-        let inner = NSStackView(views: [titleField, descriptionField])
316
-        inner.orientation = .vertical
317
-        inner.spacing = 6
318
-        inner.alignment = .leading
319
-        inner.translatesAutoresizingMaskIntoConstraints = false
320
-
321
-        card.addSubview(inner)
331
+        let applyButton = JobPayloadButton(title: "Apply", target: self, action: #selector(didTapJobApply(_:)))
332
+        applyButton.jobPayload = job
333
+        applyButton.isBordered = false
334
+        applyButton.bezelStyle = .rounded
335
+        applyButton.font = .systemFont(ofSize: 13, weight: .semibold)
336
+        applyButton.wantsLayer = true
337
+        applyButton.layer?.cornerRadius = 6
338
+        applyButton.layer?.backgroundColor = Theme.brandBlue.cgColor
339
+        applyButton.contentTintColor = Theme.proCTAText
340
+        applyButton.focusRingType = .none
341
+        applyButton.setContentHuggingPriority(.required, for: .horizontal)
342
+        applyButton.setContentCompressionResistancePriority(.required, for: .horizontal)
343
+
344
+        let savedButton = JobPayloadButton(title: "Saved", target: self, action: #selector(didTapJobSaved(_:)))
345
+        savedButton.jobPayload = job
346
+        savedButton.setButtonType(.toggle)
347
+        savedButton.isBordered = false
348
+        savedButton.bezelStyle = .rounded
349
+        savedButton.font = .systemFont(ofSize: 13, weight: .semibold)
350
+        savedButton.focusRingType = .none
351
+        savedButton.state = savedJobs.contains(job) ? .on : .off
352
+        styleJobSavedButton(savedButton)
353
+        savedButton.setContentHuggingPriority(.required, for: .horizontal)
354
+        savedButton.setContentCompressionResistancePriority(.required, for: .horizontal)
355
+
356
+        let dismissButton = JobPayloadButton()
357
+        dismissButton.jobPayload = job
358
+        dismissButton.image = NSImage(systemSymbolName: "xmark", accessibilityDescription: "Dismiss")
359
+        dismissButton.imagePosition = .imageOnly
360
+        dismissButton.imageScaling = .scaleProportionallyDown
361
+        dismissButton.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 11, weight: .semibold)
362
+        dismissButton.isBordered = false
363
+        dismissButton.bezelStyle = .rounded
364
+        dismissButton.contentTintColor = Theme.secondaryText
365
+        dismissButton.target = self
366
+        dismissButton.action = #selector(didTapJobDismiss(_:))
367
+        dismissButton.toolTip = "Dismiss"
368
+        dismissButton.focusRingType = .none
369
+        dismissButton.setContentHuggingPriority(.required, for: .horizontal)
370
+
371
+        let buttonRow = NSStackView(views: [applyButton, savedButton, dismissButton])
372
+        buttonRow.orientation = .horizontal
373
+        buttonRow.spacing = 8
374
+        buttonRow.alignment = .centerY
375
+        buttonRow.translatesAutoresizingMaskIntoConstraints = false
376
+        buttonRow.setContentHuggingPriority(.required, for: .horizontal)
377
+        buttonRow.setContentCompressionResistancePriority(.required, for: .horizontal)
378
+
379
+        titleField.setContentHuggingPriority(.defaultLow, for: .horizontal)
380
+        titleField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
381
+
382
+        let titleAndActionsRow = NSStackView(views: [titleField, buttonRow])
383
+        titleAndActionsRow.orientation = .horizontal
384
+        titleAndActionsRow.spacing = 14
385
+        titleAndActionsRow.alignment = .centerY
386
+        titleAndActionsRow.distribution = .fill
387
+        titleAndActionsRow.translatesAutoresizingMaskIntoConstraints = false
388
+
389
+        let contentColumn = NSStackView(views: [titleAndActionsRow, descriptionField])
390
+        contentColumn.orientation = .vertical
391
+        contentColumn.spacing = 6
392
+        contentColumn.alignment = .width
393
+        contentColumn.translatesAutoresizingMaskIntoConstraints = false
394
+
395
+        card.addSubview(contentColumn)
322 396
         NSLayoutConstraint.activate([
323
-            inner.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 16),
324
-            inner.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -16),
325
-            inner.topAnchor.constraint(equalTo: card.topAnchor, constant: 14),
326
-            inner.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -14),
327
-
328
-            titleField.leadingAnchor.constraint(equalTo: inner.leadingAnchor),
329
-            titleField.trailingAnchor.constraint(equalTo: inner.trailingAnchor),
330
-            descriptionField.leadingAnchor.constraint(equalTo: inner.leadingAnchor),
331
-            descriptionField.trailingAnchor.constraint(equalTo: inner.trailingAnchor)
397
+            contentColumn.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 16),
398
+            contentColumn.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -16),
399
+            contentColumn.topAnchor.constraint(equalTo: card.topAnchor, constant: 14),
400
+            contentColumn.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -14),
401
+
402
+            applyButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 72),
403
+            applyButton.heightAnchor.constraint(equalToConstant: 28),
404
+            savedButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 72),
405
+            savedButton.heightAnchor.constraint(equalToConstant: 28),
406
+            dismissButton.widthAnchor.constraint(equalToConstant: 28),
407
+            dismissButton.heightAnchor.constraint(equalToConstant: 28)
332 408
         ])
333 409
 
334 410
         return card
335 411
     }
336 412
 
413
+    private func styleJobSavedButton(_ button: NSButton) {
414
+        button.wantsLayer = true
415
+        button.layer?.cornerRadius = 6
416
+        let on = button.state == .on
417
+        if on {
418
+            button.layer?.backgroundColor = Theme.selectionFill.cgColor
419
+            button.layer?.borderWidth = 1
420
+            button.layer?.borderColor = Theme.brandBlue.cgColor
421
+            button.contentTintColor = Theme.brandBlue
422
+        } else {
423
+            button.layer?.backgroundColor = Theme.cardBackground.cgColor
424
+            button.layer?.borderWidth = 1
425
+            button.layer?.borderColor = Theme.border.cgColor
426
+            button.contentTintColor = Theme.primaryText
427
+        }
428
+    }
429
+
430
+    @objc private func didTapJobApply(_ sender: NSButton) {
431
+        guard let job = (sender as? JobPayloadButton)?.jobPayload else { return }
432
+        let allowed = CharacterSet.urlQueryAllowed
433
+        let q = job.title.addingPercentEncoding(withAllowedCharacters: allowed) ?? ""
434
+        guard let url = URL(string: "https://www.indeed.com/jobs?q=\(q)") else { return }
435
+        NSWorkspace.shared.open(url)
436
+    }
437
+
438
+    @objc private func didTapJobSaved(_ sender: NSButton) {
439
+        guard let job = (sender as? JobPayloadButton)?.jobPayload else { return }
440
+        if savedJobs.contains(job) {
441
+            savedJobs.remove(job)
442
+        } else {
443
+            savedJobs.insert(job)
444
+        }
445
+        sender.state = savedJobs.contains(job) ? .on : .off
446
+        styleJobSavedButton(sender)
447
+    }
448
+
449
+    @objc private func didTapJobDismiss(_ sender: NSButton) {
450
+        guard let job = (sender as? JobPayloadButton)?.jobPayload else { return }
451
+        lastSearchResults.removeAll { $0 == job }
452
+        configureJobListings(lastSearchResults, noResultsForQuery: lastNoResultsQuery)
453
+    }
454
+
337 455
     private func configureSearchBar() {
338 456
         let pillCorner: CGFloat = 27
339 457
         let barHeight: CGFloat = 54
@@ -820,6 +938,11 @@ final class DashboardView: NSView, NSTextFieldDelegate {
820 938
 
821 939
 }
822 940
 
941
+/// `NSButton` that carries a `JobListing` for card actions (`representedObject` is unavailable on `NSButton` in this target).
942
+private final class JobPayloadButton: NSButton {
943
+    var jobPayload: JobListing?
944
+}
945
+
823 946
 /// 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).
824 947
 private final class JobListingsDocumentView: NSView {
825 948
     override var isFlipped: Bool { true }