|
|
@@ -20,8 +20,8 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
20
|
20
|
static let chromeBackground = NSColor(srgbRed: 247 / 255, green: 247 / 255, blue: 247 / 255, alpha: 1)
|
|
21
|
21
|
static let sidebarBackground = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
|
|
22
|
22
|
static let mainHostBackground = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
|
|
23
|
|
- /// Subtitle on the welcome hero: readable blue-gray aligned with brand.
|
|
24
|
|
- static let welcomeSubtitleText = NSColor(srgbRed: 52 / 255, green: 92 / 255, blue: 142 / 255, alpha: 1)
|
|
|
23
|
+ /// Subtitle on the welcome hero: dark neutral gray to match the reference layout.
|
|
|
24
|
+ static let welcomeSubtitleText = NSColor(srgbRed: 64 / 255, green: 64 / 255, blue: 64 / 255, alpha: 1)
|
|
25
|
25
|
static let selectionFill = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 0.12)
|
|
26
|
26
|
static let cardBackground = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
|
|
27
|
27
|
static let toggleBackground = NSColor(srgbRed: 232 / 255, green: 232 / 255, blue: 232 / 255, alpha: 1)
|
|
|
@@ -29,10 +29,13 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
29
|
29
|
static let secondaryText = NSColor(srgbRed: 118 / 255, green: 118 / 255, blue: 118 / 255, alpha: 1)
|
|
30
|
30
|
static let tertiaryText = NSColor(srgbRed: 118 / 255, green: 118 / 255, blue: 118 / 255, alpha: 1)
|
|
31
|
31
|
static let border = NSColor(srgbRed: 212 / 255, green: 210 / 255, blue: 208 / 255, alpha: 1)
|
|
32
|
|
- /// Job search bar outer stroke (charcoal).
|
|
33
|
|
- static let searchBarBorder = NSColor(srgbRed: 58 / 255, green: 58 / 255, blue: 58 / 255, alpha: 1)
|
|
|
32
|
+ /// Home status strip and soft accents (light blue panel in the reference UI).
|
|
|
33
|
+ static let statusBannerBackground = NSColor(srgbRed: 232 / 255, green: 242 / 255, blue: 252 / 255, alpha: 1)
|
|
|
34
|
+ static let featureIconWell = NSColor(srgbRed: 220 / 255, green: 235 / 255, blue: 252 / 255, alpha: 1)
|
|
|
35
|
+ /// Job search bar outer stroke (soft blue-gray, pill field in the reference UI).
|
|
|
36
|
+ static let searchBarBorder = NSColor(srgbRed: 180 / 255, green: 200 / 255, blue: 228 / 255, alpha: 1)
|
|
34
|
37
|
/// Search bar border on hover (brand-tinted, matches focus affordance elsewhere).
|
|
35
|
|
- static let searchBarBorderHover = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 0.45)
|
|
|
38
|
+ static let searchBarBorderHover = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 0.55)
|
|
36
|
39
|
static let proCardFill = NSColor(srgbRed: 239 / 255, green: 244 / 255, blue: 252 / 255, alpha: 1)
|
|
37
|
40
|
static let proCardBorder = NSColor(srgbRed: 212 / 255, green: 210 / 255, blue: 208 / 255, alpha: 1)
|
|
38
|
41
|
static let proAccent = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 1)
|
|
|
@@ -81,11 +84,18 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
81
|
84
|
private let findJobsCTAHost = NSView()
|
|
82
|
85
|
private let findJobsCTAChrome = HoverableView()
|
|
83
|
86
|
private var findJobsCTAGradientLayer: CAGradientLayer?
|
|
84
|
|
- private let chatStatusStack = NSStackView()
|
|
|
87
|
+ private let statusBannerContainer = NSView()
|
|
|
88
|
+ private let statusBannerInner = NSView()
|
|
|
89
|
+ private let statusBannerRow = NSStackView()
|
|
85
|
90
|
private let chatStatusSymbolContainer = NSView()
|
|
86
|
91
|
private let chatStatusIcon = NSImageView()
|
|
87
|
92
|
private let chatStatusLoadingIndicator = NSProgressIndicator()
|
|
88
|
|
- private let chatStatusLabel = NSTextField(labelWithString: "Opening the vault...")
|
|
|
93
|
+ private let chatStatusLabel = NSTextField(wrappingLabelWithString: "Opening the vault...")
|
|
|
94
|
+ private let statusBrandTrailing = NSStackView()
|
|
|
95
|
+ private let statusBrandStarIcon = NSImageView()
|
|
|
96
|
+ private let statusBrandLabel = NSTextField(labelWithString: "AI Job Finder")
|
|
|
97
|
+ private let welcomeSparkleIcon = NSImageView()
|
|
|
98
|
+ private let featureCardsRow = NSStackView()
|
|
89
|
99
|
private let chatScrollView = NSScrollView()
|
|
90
|
100
|
private let chatDocumentView = JobListingsDocumentView()
|
|
91
|
101
|
private let chatStack = NSStackView()
|
|
|
@@ -145,6 +155,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
145
|
155
|
updateFindJobsCTAShadowPath()
|
|
146
|
156
|
updateJobListingDescriptionWidths()
|
|
147
|
157
|
updateChatBubbleWidths()
|
|
|
158
|
+ updateStatusBannerLabelWidth()
|
|
148
|
159
|
}
|
|
149
|
160
|
|
|
150
|
161
|
func render(_ data: DashboardData) {
|
|
|
@@ -226,14 +237,21 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
226
|
237
|
configureSearchBar()
|
|
227
|
238
|
configureChatViews()
|
|
228
|
239
|
|
|
229
|
|
- let titleBlock = NSStackView(views: [greetingLabel, subtitleLabel])
|
|
|
240
|
+ welcomeSparkleIcon.translatesAutoresizingMaskIntoConstraints = false
|
|
|
241
|
+ welcomeSparkleIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 16, weight: .semibold)
|
|
|
242
|
+ welcomeSparkleIcon.image = NSImage(systemSymbolName: "sparkles", accessibilityDescription: nil)
|
|
|
243
|
+ welcomeSparkleIcon.contentTintColor = Theme.brandBlue
|
|
|
244
|
+
|
|
|
245
|
+ let titleBlock = NSStackView(views: [greetingLabel, subtitleLabel, welcomeSparkleIcon])
|
|
230
|
246
|
titleBlock.orientation = .vertical
|
|
231
|
247
|
titleBlock.spacing = 10
|
|
232
|
248
|
titleBlock.alignment = .centerX
|
|
233
|
249
|
|
|
|
250
|
+ configureFeatureShortcutCards()
|
|
|
251
|
+
|
|
234
|
252
|
let midSpacer = NSView()
|
|
235
|
253
|
midSpacer.translatesAutoresizingMaskIntoConstraints = false
|
|
236
|
|
- midSpacer.heightAnchor.constraint(equalToConstant: 18).isActive = true
|
|
|
254
|
+ midSpacer.heightAnchor.constraint(equalToConstant: 12).isActive = true
|
|
237
|
255
|
|
|
238
|
256
|
let chatTopSpacer = NSView()
|
|
239
|
257
|
chatTopSpacer.translatesAutoresizingMaskIntoConstraints = false
|
|
|
@@ -245,8 +263,9 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
245
|
263
|
|
|
246
|
264
|
mainOverlay.addArrangedSubview(topInset)
|
|
247
|
265
|
mainOverlay.addArrangedSubview(titleBlock)
|
|
|
266
|
+ mainOverlay.addArrangedSubview(featureCardsRow)
|
|
248
|
267
|
mainOverlay.addArrangedSubview(midSpacer)
|
|
249
|
|
- mainOverlay.addArrangedSubview(chatStatusStack)
|
|
|
268
|
+ mainOverlay.addArrangedSubview(statusBannerContainer)
|
|
250
|
269
|
mainOverlay.addArrangedSubview(chatTopSpacer)
|
|
251
|
270
|
mainOverlay.addArrangedSubview(chatScrollView)
|
|
252
|
271
|
mainOverlay.addArrangedSubview(chatBottomSpacer)
|
|
|
@@ -280,7 +299,8 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
280
|
299
|
nonHomeHost.bottomAnchor.constraint(equalTo: mainHost.bottomAnchor, constant: -24),
|
|
281
|
300
|
|
|
282
|
301
|
searchBarShadowHost.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
|
|
283
|
|
- chatStatusStack.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
|
|
|
302
|
+ statusBannerContainer.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
|
|
|
303
|
+ featureCardsRow.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
|
|
284
|
304
|
chatScrollView.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
|
|
285
|
305
|
|
|
286
|
306
|
greetingLabel.leadingAnchor.constraint(equalTo: mainOverlay.leadingAnchor, constant: 16),
|
|
|
@@ -290,19 +310,60 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
290
|
310
|
])
|
|
291
|
311
|
}
|
|
292
|
312
|
|
|
|
313
|
+ private func configureFeatureShortcutCards() {
|
|
|
314
|
+ featureCardsRow.orientation = .horizontal
|
|
|
315
|
+ featureCardsRow.spacing = 12
|
|
|
316
|
+ featureCardsRow.distribution = .fillEqually
|
|
|
317
|
+ featureCardsRow.alignment = .top
|
|
|
318
|
+ featureCardsRow.translatesAutoresizingMaskIntoConstraints = false
|
|
|
319
|
+
|
|
|
320
|
+ let specs: [(symbol: String, title: String, subtitle: String, action: Selector)] = [
|
|
|
321
|
+ ("briefcase.fill", "Role", "Explore similar or better job roles", #selector(didTapFeatureRole)),
|
|
|
322
|
+ ("building.2.fill", "Company", "Find opportunities at other companies", #selector(didTapFeatureCompany)),
|
|
|
323
|
+ ("chevron.left.forwardslash.chevron.right", "Skill", "Match jobs that fit your skills", #selector(didTapFeatureSkill))
|
|
|
324
|
+ ]
|
|
|
325
|
+ for spec in specs {
|
|
|
326
|
+ let card = FeatureShortcutCardView(
|
|
|
327
|
+ symbolName: spec.symbol,
|
|
|
328
|
+ title: spec.title,
|
|
|
329
|
+ subtitle: spec.subtitle,
|
|
|
330
|
+ target: self,
|
|
|
331
|
+ action: spec.action
|
|
|
332
|
+ )
|
|
|
333
|
+ featureCardsRow.addArrangedSubview(card)
|
|
|
334
|
+ }
|
|
|
335
|
+ }
|
|
|
336
|
+
|
|
293
|
337
|
private func configureChatViews() {
|
|
294
|
|
- chatStatusStack.orientation = .vertical
|
|
295
|
|
- chatStatusStack.spacing = 6
|
|
296
|
|
- chatStatusStack.alignment = .centerX
|
|
297
|
|
- chatStatusStack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
338
|
+ statusBannerContainer.translatesAutoresizingMaskIntoConstraints = false
|
|
|
339
|
+ statusBannerContainer.setContentHuggingPriority(.defaultHigh, for: .vertical)
|
|
|
340
|
+
|
|
|
341
|
+ statusBannerInner.translatesAutoresizingMaskIntoConstraints = false
|
|
|
342
|
+ statusBannerInner.wantsLayer = true
|
|
|
343
|
+ statusBannerInner.layer?.backgroundColor = Theme.statusBannerBackground.cgColor
|
|
|
344
|
+ statusBannerInner.layer?.cornerRadius = 14
|
|
|
345
|
+ if #available(macOS 11.0, *) {
|
|
|
346
|
+ statusBannerInner.layer?.cornerCurve = .continuous
|
|
|
347
|
+ }
|
|
|
348
|
+ statusBannerInner.layer?.masksToBounds = true
|
|
|
349
|
+
|
|
|
350
|
+ statusBannerContainer.addSubview(statusBannerInner)
|
|
|
351
|
+
|
|
|
352
|
+ statusBannerRow.orientation = .horizontal
|
|
|
353
|
+ statusBannerRow.spacing = 12
|
|
|
354
|
+ statusBannerRow.alignment = .top
|
|
|
355
|
+ statusBannerRow.translatesAutoresizingMaskIntoConstraints = false
|
|
298
|
356
|
|
|
299
|
357
|
chatStatusSymbolContainer.translatesAutoresizingMaskIntoConstraints = false
|
|
|
358
|
+ chatStatusSymbolContainer.wantsLayer = true
|
|
|
359
|
+ chatStatusSymbolContainer.layer?.backgroundColor = Theme.brandBlue.cgColor
|
|
|
360
|
+ chatStatusSymbolContainer.layer?.cornerRadius = 20
|
|
300
|
361
|
|
|
301
|
362
|
chatStatusIcon.translatesAutoresizingMaskIntoConstraints = false
|
|
302
|
363
|
chatStatusIcon.wantsLayer = true
|
|
303
|
|
- chatStatusIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 36, weight: .regular)
|
|
|
364
|
+ chatStatusIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 15, weight: .semibold)
|
|
304
|
365
|
chatStatusIcon.image = NSImage(systemSymbolName: "sparkles", accessibilityDescription: "Assistant status")
|
|
305
|
|
- chatStatusIcon.contentTintColor = Theme.brandBlue
|
|
|
366
|
+ chatStatusIcon.contentTintColor = .white
|
|
306
|
367
|
|
|
307
|
368
|
chatStatusLoadingIndicator.translatesAutoresizingMaskIntoConstraints = false
|
|
308
|
369
|
chatStatusLoadingIndicator.style = .spinning
|
|
|
@@ -323,19 +384,66 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
323
|
384
|
chatStatusIcon.centerYAnchor.constraint(equalTo: chatStatusSymbolContainer.centerYAnchor),
|
|
324
|
385
|
chatStatusLoadingIndicator.centerXAnchor.constraint(equalTo: chatStatusSymbolContainer.centerXAnchor),
|
|
325
|
386
|
chatStatusLoadingIndicator.centerYAnchor.constraint(equalTo: chatStatusSymbolContainer.centerYAnchor),
|
|
326
|
|
- chatStatusLoadingIndicator.widthAnchor.constraint(equalToConstant: 32),
|
|
327
|
|
- chatStatusLoadingIndicator.heightAnchor.constraint(equalToConstant: 32)
|
|
|
387
|
+ chatStatusLoadingIndicator.widthAnchor.constraint(equalToConstant: 26),
|
|
|
388
|
+ chatStatusLoadingIndicator.heightAnchor.constraint(equalToConstant: 26)
|
|
328
|
389
|
])
|
|
329
|
390
|
|
|
330
|
|
- chatStatusLabel.font = .systemFont(ofSize: 20, weight: .semibold)
|
|
|
391
|
+ chatStatusLabel.font = .systemFont(ofSize: 13, weight: .regular)
|
|
331
|
392
|
chatStatusLabel.textColor = Theme.primaryText
|
|
332
|
|
- chatStatusLabel.alignment = .center
|
|
333
|
|
- chatStatusLabel.maximumNumberOfLines = 1
|
|
|
393
|
+ chatStatusLabel.alignment = .left
|
|
|
394
|
+ chatStatusLabel.maximumNumberOfLines = 0
|
|
|
395
|
+ chatStatusLabel.lineBreakMode = .byWordWrapping
|
|
|
396
|
+ chatStatusLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
397
|
+ chatStatusLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
|
398
|
+ chatStatusLabel.translatesAutoresizingMaskIntoConstraints = false
|
|
|
399
|
+
|
|
|
400
|
+ statusBrandStarIcon.translatesAutoresizingMaskIntoConstraints = false
|
|
|
401
|
+ statusBrandStarIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 12, weight: .semibold)
|
|
|
402
|
+ statusBrandStarIcon.image = NSImage(systemSymbolName: "sparkle", accessibilityDescription: nil)
|
|
|
403
|
+ statusBrandStarIcon.contentTintColor = Theme.brandBlue
|
|
|
404
|
+
|
|
|
405
|
+ statusBrandLabel.font = .systemFont(ofSize: 12, weight: .semibold)
|
|
|
406
|
+ statusBrandLabel.textColor = Theme.brandBlue
|
|
|
407
|
+ statusBrandLabel.alignment = .right
|
|
|
408
|
+ statusBrandLabel.maximumNumberOfLines = 1
|
|
|
409
|
+
|
|
|
410
|
+ statusBrandTrailing.orientation = .horizontal
|
|
|
411
|
+ statusBrandTrailing.spacing = 5
|
|
|
412
|
+ statusBrandTrailing.alignment = .centerY
|
|
|
413
|
+ statusBrandTrailing.translatesAutoresizingMaskIntoConstraints = false
|
|
|
414
|
+ statusBrandTrailing.addArrangedSubview(statusBrandStarIcon)
|
|
|
415
|
+ statusBrandTrailing.addArrangedSubview(statusBrandLabel)
|
|
|
416
|
+ statusBrandTrailing.setContentHuggingPriority(.required, for: .horizontal)
|
|
|
417
|
+ statusBrandTrailing.setContentCompressionResistancePriority(.required, for: .horizontal)
|
|
|
418
|
+
|
|
|
419
|
+ statusBannerRow.addArrangedSubview(chatStatusSymbolContainer)
|
|
|
420
|
+ statusBannerRow.addArrangedSubview(chatStatusLabel)
|
|
|
421
|
+
|
|
|
422
|
+ statusBannerInner.addSubview(statusBannerRow)
|
|
|
423
|
+ statusBannerContainer.addSubview(statusBannerInner)
|
|
|
424
|
+ statusBannerContainer.addSubview(statusBrandTrailing)
|
|
|
425
|
+ statusBannerInner.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
|
|
426
|
+ statusBannerInner.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
334
|
427
|
|
|
335
|
|
- chatStatusStack.addArrangedSubview(chatStatusSymbolContainer)
|
|
336
|
|
- chatStatusStack.addArrangedSubview(chatStatusLabel)
|
|
|
428
|
+ NSLayoutConstraint.activate([
|
|
|
429
|
+ statusBannerInner.leadingAnchor.constraint(equalTo: statusBannerContainer.leadingAnchor),
|
|
|
430
|
+ statusBannerInner.topAnchor.constraint(equalTo: statusBannerContainer.topAnchor),
|
|
|
431
|
+ statusBannerInner.bottomAnchor.constraint(equalTo: statusBannerContainer.bottomAnchor),
|
|
|
432
|
+ statusBannerInner.trailingAnchor.constraint(lessThanOrEqualTo: statusBrandTrailing.leadingAnchor, constant: -16),
|
|
|
433
|
+
|
|
|
434
|
+ statusBrandTrailing.trailingAnchor.constraint(equalTo: statusBannerContainer.trailingAnchor),
|
|
|
435
|
+ statusBrandTrailing.centerYAnchor.constraint(equalTo: statusBannerContainer.centerYAnchor),
|
|
|
436
|
+
|
|
|
437
|
+ statusBannerRow.leadingAnchor.constraint(equalTo: statusBannerInner.leadingAnchor, constant: 14),
|
|
|
438
|
+ statusBannerRow.trailingAnchor.constraint(equalTo: statusBannerInner.trailingAnchor, constant: -16),
|
|
|
439
|
+ statusBannerRow.topAnchor.constraint(equalTo: statusBannerInner.topAnchor, constant: 10),
|
|
|
440
|
+ statusBannerRow.bottomAnchor.constraint(equalTo: statusBannerInner.bottomAnchor, constant: -10),
|
|
|
441
|
+
|
|
|
442
|
+ statusBannerContainer.heightAnchor.constraint(greaterThanOrEqualToConstant: 52)
|
|
|
443
|
+ ])
|
|
337
|
444
|
|
|
338
|
445
|
syncChatStatusLoadingIndicator(forStatusText: chatStatusLabel.stringValue)
|
|
|
446
|
+ syncChatStatusBannerVisuals(forStatusText: chatStatusLabel.stringValue)
|
|
339
|
447
|
|
|
340
|
448
|
chatDocumentView.translatesAutoresizingMaskIntoConstraints = false
|
|
341
|
449
|
chatStack.orientation = .vertical
|
|
|
@@ -374,7 +482,43 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
374
|
482
|
private func setChatStatusLabel(_ text: String) {
|
|
375
|
483
|
chatStatusLabel.stringValue = text
|
|
376
|
484
|
syncChatStatusLoadingIndicator(forStatusText: text)
|
|
|
485
|
+ syncChatStatusBannerVisuals(forStatusText: text)
|
|
377
|
486
|
syncChatStatusSparkleAnimation()
|
|
|
487
|
+ updateStatusBannerLabelWidth()
|
|
|
488
|
+ }
|
|
|
489
|
+
|
|
|
490
|
+ private func updateStatusBannerLabelWidth() {
|
|
|
491
|
+ guard statusBannerContainer.bounds.width > 1 else { return }
|
|
|
492
|
+ let trailingWidth = max(96, statusBrandTrailing.fittingSize.width)
|
|
|
493
|
+ let chrome: CGFloat = 14 + 40 + 12 + 16 + 16 + trailingWidth
|
|
|
494
|
+ let target = max(80, statusBannerContainer.bounds.width - chrome)
|
|
|
495
|
+ if abs(chatStatusLabel.preferredMaxLayoutWidth - target) > 0.5 {
|
|
|
496
|
+ chatStatusLabel.preferredMaxLayoutWidth = target
|
|
|
497
|
+ chatStatusLabel.invalidateIntrinsicContentSize()
|
|
|
498
|
+ }
|
|
|
499
|
+ }
|
|
|
500
|
+
|
|
|
501
|
+ /// Leading status glyph: sparkles for prompts, magnifying glass for search-result summaries and errors.
|
|
|
502
|
+ private func syncChatStatusBannerVisuals(forStatusText text: String) {
|
|
|
503
|
+ if text == "Thinking..." { return }
|
|
|
504
|
+ let lower = text.lowercased()
|
|
|
505
|
+ let useMagnifyingGlass =
|
|
|
506
|
+ text.hasPrefix("Found ")
|
|
|
507
|
+ || text.hasPrefix("Here are")
|
|
|
508
|
+ || text.hasPrefix("No jobs")
|
|
|
509
|
+ || text.contains("couldn't find")
|
|
|
510
|
+ || lower.contains("try again")
|
|
|
511
|
+ || lower.contains("could not reach")
|
|
|
512
|
+ || lower.contains("search did not finish")
|
|
|
513
|
+ let config = NSImage.SymbolConfiguration(pointSize: 15, weight: .semibold)
|
|
|
514
|
+ chatStatusIcon.symbolConfiguration = config
|
|
|
515
|
+ if useMagnifyingGlass {
|
|
|
516
|
+ chatStatusIcon.image = NSImage(systemSymbolName: "magnifyingglass", accessibilityDescription: "Search status")
|
|
|
517
|
+ } else {
|
|
|
518
|
+ chatStatusIcon.image = NSImage(systemSymbolName: "sparkles", accessibilityDescription: "Assistant status")
|
|
|
519
|
+ }
|
|
|
520
|
+ chatStatusIcon.contentTintColor = .white
|
|
|
521
|
+ chatStatusSymbolContainer.layer?.backgroundColor = Theme.brandBlue.cgColor
|
|
378
|
522
|
}
|
|
379
|
523
|
|
|
380
|
524
|
private func syncChatStatusLoadingIndicator(forStatusText text: String) {
|
|
|
@@ -383,9 +527,12 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
383
|
527
|
chatStatusIcon.isHidden = true
|
|
384
|
528
|
chatStatusLoadingIndicator.isHidden = false
|
|
385
|
529
|
chatStatusLoadingIndicator.startAnimation(nil)
|
|
|
530
|
+ chatStatusSymbolContainer.layer?.backgroundColor = Theme.featureIconWell.cgColor
|
|
386
|
531
|
} else {
|
|
387
|
532
|
chatStatusLoadingIndicator.stopAnimation(nil)
|
|
388
|
533
|
chatStatusIcon.isHidden = false
|
|
|
534
|
+ chatStatusLoadingIndicator.isHidden = true
|
|
|
535
|
+ chatStatusSymbolContainer.layer?.backgroundColor = Theme.brandBlue.cgColor
|
|
389
|
536
|
}
|
|
390
|
537
|
}
|
|
391
|
538
|
|
|
|
@@ -399,7 +546,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
399
|
546
|
|
|
400
|
547
|
private func shouldAnimateChatStatusSparkles(for statusText: String) -> Bool {
|
|
401
|
548
|
statusText == "Ask me to find jobs"
|
|
402
|
|
- || statusText == "Ask for another role, company, or skill match"
|
|
|
549
|
+ || statusText == "Opening the vault..."
|
|
403
|
550
|
}
|
|
404
|
551
|
|
|
405
|
552
|
private func syncChatStatusSparkleAnimation() {
|
|
|
@@ -542,30 +689,99 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
542
|
689
|
persistSavedJobs()
|
|
543
|
690
|
}
|
|
544
|
691
|
|
|
|
692
|
+ private func jobListingHostSubtitle(_ job: JobListing) -> String {
|
|
|
693
|
+ guard let raw = job.url, let url = URL(string: raw), let host = url.host?.lowercased() else {
|
|
|
694
|
+ return "Indeed"
|
|
|
695
|
+ }
|
|
|
696
|
+ if host.hasPrefix("www.") {
|
|
|
697
|
+ return String(host.dropFirst(4))
|
|
|
698
|
+ }
|
|
|
699
|
+ return host
|
|
|
700
|
+ }
|
|
|
701
|
+
|
|
|
702
|
+ private func jobListingCategorySymbol(for job: JobListing) -> String {
|
|
|
703
|
+ let blob = (job.title + " " + job.description).lowercased()
|
|
|
704
|
+ if blob.contains("machine learning") || blob.contains("deep learning") || blob.contains(" ml ") {
|
|
|
705
|
+ return "brain.head.profile"
|
|
|
706
|
+ }
|
|
|
707
|
+ if blob.contains("audio") || blob.contains(" sound ") || blob.contains("dsp") {
|
|
|
708
|
+ return "waveform"
|
|
|
709
|
+ }
|
|
|
710
|
+ if blob.contains("ios") || blob.contains("swift") || blob.contains("mobile") {
|
|
|
711
|
+ return "iphone"
|
|
|
712
|
+ }
|
|
|
713
|
+ if blob.contains("design") || blob.contains(" ux") || blob.contains("figma") {
|
|
|
714
|
+ return "paintpalette.fill"
|
|
|
715
|
+ }
|
|
|
716
|
+ if blob.contains("data ") || blob.contains("analytics") {
|
|
|
717
|
+ return "chart.bar.fill"
|
|
|
718
|
+ }
|
|
|
719
|
+ if blob.contains("ai") || blob.contains("llm") || blob.contains("nlp") {
|
|
|
720
|
+ return "cpu"
|
|
|
721
|
+ }
|
|
|
722
|
+ return "briefcase.fill"
|
|
|
723
|
+ }
|
|
|
724
|
+
|
|
545
|
725
|
private func makeJobListingCard(_ job: JobListing, context: JobListingCardContext) -> NSView {
|
|
546
|
726
|
let card = NSView()
|
|
547
|
727
|
card.translatesAutoresizingMaskIntoConstraints = false
|
|
548
|
728
|
card.wantsLayer = true
|
|
549
|
729
|
card.layer?.backgroundColor = Theme.cardBackground.cgColor
|
|
550
|
|
- card.layer?.cornerRadius = 12
|
|
|
730
|
+ card.layer?.cornerRadius = 14
|
|
551
|
731
|
card.layer?.borderWidth = 1
|
|
552
|
732
|
card.layer?.borderColor = Theme.border.cgColor
|
|
553
|
733
|
card.layer?.masksToBounds = true
|
|
554
|
734
|
|
|
|
735
|
+ let iconBox = NSView()
|
|
|
736
|
+ iconBox.translatesAutoresizingMaskIntoConstraints = false
|
|
|
737
|
+ iconBox.wantsLayer = true
|
|
|
738
|
+ iconBox.layer?.backgroundColor = Theme.brandBlue.cgColor
|
|
|
739
|
+ iconBox.layer?.cornerRadius = 12
|
|
|
740
|
+ if #available(macOS 11.0, *) {
|
|
|
741
|
+ iconBox.layer?.cornerCurve = .continuous
|
|
|
742
|
+ }
|
|
|
743
|
+
|
|
|
744
|
+ let categoryIcon = NSImageView()
|
|
|
745
|
+ categoryIcon.translatesAutoresizingMaskIntoConstraints = false
|
|
|
746
|
+ categoryIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 22, weight: .medium)
|
|
|
747
|
+ categoryIcon.image = NSImage(systemSymbolName: jobListingCategorySymbol(for: job), accessibilityDescription: nil)
|
|
|
748
|
+ categoryIcon.contentTintColor = .white
|
|
|
749
|
+ iconBox.addSubview(categoryIcon)
|
|
|
750
|
+
|
|
555
|
751
|
let titleField = NSTextField(labelWithString: job.title)
|
|
556
|
752
|
titleField.font = .systemFont(ofSize: 16, weight: .semibold)
|
|
557
|
|
- titleField.textColor = Theme.primaryText
|
|
|
753
|
+ titleField.textColor = Theme.brandBlue
|
|
558
|
754
|
titleField.maximumNumberOfLines = 2
|
|
559
|
755
|
titleField.lineBreakMode = .byWordWrapping
|
|
560
|
756
|
titleField.alignment = .left
|
|
|
757
|
+ titleField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
561
|
758
|
titleField.translatesAutoresizingMaskIntoConstraints = false
|
|
562
|
759
|
|
|
|
760
|
+ let buildingIcon = NSImageView()
|
|
|
761
|
+ buildingIcon.translatesAutoresizingMaskIntoConstraints = false
|
|
|
762
|
+ buildingIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 12, weight: .medium)
|
|
|
763
|
+ buildingIcon.image = NSImage(systemSymbolName: "building.2.fill", accessibilityDescription: nil)
|
|
|
764
|
+ buildingIcon.contentTintColor = Theme.welcomeSubtitleText
|
|
|
765
|
+
|
|
|
766
|
+ let companyLabel = NSTextField(labelWithString: jobListingHostSubtitle(job))
|
|
|
767
|
+ companyLabel.font = .systemFont(ofSize: 12, weight: .medium)
|
|
|
768
|
+ companyLabel.textColor = Theme.welcomeSubtitleText
|
|
|
769
|
+ companyLabel.maximumNumberOfLines = 1
|
|
|
770
|
+ companyLabel.lineBreakMode = .byTruncatingTail
|
|
|
771
|
+ companyLabel.translatesAutoresizingMaskIntoConstraints = false
|
|
|
772
|
+
|
|
|
773
|
+ let companyRow = NSStackView(views: [buildingIcon, companyLabel])
|
|
|
774
|
+ companyRow.orientation = .horizontal
|
|
|
775
|
+ companyRow.spacing = 5
|
|
|
776
|
+ companyRow.alignment = .centerY
|
|
|
777
|
+ companyRow.translatesAutoresizingMaskIntoConstraints = false
|
|
|
778
|
+
|
|
563
|
779
|
let descriptionField = NSTextField(wrappingLabelWithString: job.description)
|
|
564
|
780
|
descriptionField.font = .systemFont(ofSize: 13, weight: .regular)
|
|
565
|
781
|
descriptionField.textColor = Theme.secondaryText
|
|
566
|
|
- descriptionField.maximumNumberOfLines = 0
|
|
567
|
|
- descriptionField.alignment = .left
|
|
|
782
|
+ descriptionField.maximumNumberOfLines = 2
|
|
568
|
783
|
descriptionField.lineBreakMode = .byWordWrapping
|
|
|
784
|
+ descriptionField.alignment = .left
|
|
569
|
785
|
descriptionField.baseWritingDirection = .leftToRight
|
|
570
|
786
|
descriptionField.attributedStringValue = Self.jobListingDescriptionAttributedString(job.description)
|
|
571
|
787
|
if let cell = descriptionField.cell as? NSTextFieldCell {
|
|
|
@@ -584,7 +800,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
584
|
800
|
applyButton.bezelStyle = .rounded
|
|
585
|
801
|
applyButton.font = .systemFont(ofSize: 13, weight: .semibold)
|
|
586
|
802
|
applyButton.wantsLayer = true
|
|
587
|
|
- applyButton.layer?.cornerRadius = 6
|
|
|
803
|
+ applyButton.layer?.cornerRadius = 8
|
|
588
|
804
|
applyButton.layer?.backgroundColor = Theme.brandBlue.cgColor
|
|
589
|
805
|
applyButton.contentTintColor = Theme.proCTAText
|
|
590
|
806
|
applyButton.focusRingType = .none
|
|
|
@@ -603,6 +819,9 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
603
|
819
|
savedButton.isBordered = false
|
|
604
|
820
|
savedButton.bezelStyle = .rounded
|
|
605
|
821
|
savedButton.font = .systemFont(ofSize: 13, weight: .semibold)
|
|
|
822
|
+ savedButton.image = NSImage(systemSymbolName: savedOn ? "heart.fill" : "heart", accessibilityDescription: nil)
|
|
|
823
|
+ savedButton.imagePosition = .imageLeading
|
|
|
824
|
+ savedButton.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 11, weight: .semibold)
|
|
606
|
825
|
savedButton.focusRingType = .none
|
|
607
|
826
|
savedButton.state = savedOn ? .on : .off
|
|
608
|
827
|
savedButton.pointerCursor = true
|
|
|
@@ -620,7 +839,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
620
|
839
|
dismissButton.image = NSImage(systemSymbolName: "xmark", accessibilityDescription: "Dismiss")
|
|
621
|
840
|
dismissButton.imagePosition = .imageOnly
|
|
622
|
841
|
dismissButton.imageScaling = .scaleProportionallyDown
|
|
623
|
|
- dismissButton.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 11, weight: .semibold)
|
|
|
842
|
+ dismissButton.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 12, weight: .semibold)
|
|
624
|
843
|
dismissButton.isBordered = false
|
|
625
|
844
|
dismissButton.bezelStyle = .rounded
|
|
626
|
845
|
dismissButton.contentTintColor = Theme.secondaryText
|
|
|
@@ -629,7 +848,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
629
|
848
|
dismissButton.toolTip = context == .savedJobsPage ? "Remove from saved" : "Dismiss"
|
|
630
|
849
|
dismissButton.focusRingType = .none
|
|
631
|
850
|
dismissButton.wantsLayer = true
|
|
632
|
|
- dismissButton.layer?.cornerRadius = 14
|
|
|
851
|
+ dismissButton.layer?.cornerRadius = 8
|
|
633
|
852
|
dismissButton.layer?.backgroundColor = NSColor.clear.cgColor
|
|
634
|
853
|
dismissButton.pointerCursor = true
|
|
635
|
854
|
dismissButton.hoverHandler = { [weak dismissButton] hovering in
|
|
|
@@ -641,50 +860,64 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
641
|
860
|
let buttonRow = NSStackView(views: [applyButton, savedButton, dismissButton])
|
|
642
|
861
|
buttonRow.orientation = .horizontal
|
|
643
|
862
|
buttonRow.spacing = 8
|
|
644
|
|
- buttonRow.alignment = .centerY
|
|
|
863
|
+ buttonRow.alignment = .top
|
|
645
|
864
|
buttonRow.translatesAutoresizingMaskIntoConstraints = false
|
|
646
|
865
|
buttonRow.setContentHuggingPriority(.required, for: .horizontal)
|
|
647
|
866
|
buttonRow.setContentCompressionResistancePriority(.required, for: .horizontal)
|
|
|
867
|
+ buttonRow.setContentHuggingPriority(.required, for: .vertical)
|
|
|
868
|
+ buttonRow.setContentCompressionResistancePriority(.required, for: .vertical)
|
|
|
869
|
+ applyButton.setContentCompressionResistancePriority(.required, for: .horizontal)
|
|
|
870
|
+ savedButton.setContentCompressionResistancePriority(.required, for: .horizontal)
|
|
|
871
|
+ dismissButton.setContentCompressionResistancePriority(.required, for: .horizontal)
|
|
|
872
|
+
|
|
|
873
|
+ let middleColumn = NSStackView(views: [titleField, companyRow, descriptionField])
|
|
|
874
|
+ middleColumn.orientation = .vertical
|
|
|
875
|
+ middleColumn.spacing = 5
|
|
|
876
|
+ middleColumn.alignment = .leading
|
|
|
877
|
+ middleColumn.translatesAutoresizingMaskIntoConstraints = false
|
|
|
878
|
+ middleColumn.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
879
|
+ middleColumn.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
|
880
|
+
|
|
|
881
|
+ let contentRow = NSStackView(views: [iconBox, middleColumn])
|
|
|
882
|
+ contentRow.orientation = .horizontal
|
|
|
883
|
+ contentRow.spacing = 14
|
|
|
884
|
+ contentRow.alignment = .top
|
|
|
885
|
+ contentRow.distribution = .fill
|
|
|
886
|
+ contentRow.translatesAutoresizingMaskIntoConstraints = false
|
|
|
887
|
+
|
|
|
888
|
+ card.addSubview(contentRow)
|
|
|
889
|
+ card.addSubview(buttonRow)
|
|
|
890
|
+ let actionCornerInset: CGFloat = 8
|
|
|
891
|
+ let contentToActionsGap: CGFloat = 12
|
|
|
892
|
+ let bodyTrailingInset: CGFloat = 16
|
|
|
893
|
+ NSLayoutConstraint.activate([
|
|
|
894
|
+ contentRow.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 16),
|
|
|
895
|
+ contentRow.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -bodyTrailingInset),
|
|
|
896
|
+ contentRow.topAnchor.constraint(equalTo: card.topAnchor, constant: 14),
|
|
|
897
|
+ contentRow.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -14),
|
|
648
|
898
|
|
|
649
|
|
- // Title hugs the leading edge; a low–hugging-priority spacer absorbs remaining width so buttons stay trailing.
|
|
650
|
|
- titleField.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
|
651
|
|
- titleField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
|
899
|
+ buttonRow.topAnchor.constraint(equalTo: card.topAnchor, constant: actionCornerInset),
|
|
|
900
|
+ buttonRow.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -actionCornerInset),
|
|
652
|
901
|
|
|
653
|
|
- let titleRowSpacer = NSView()
|
|
654
|
|
- titleRowSpacer.translatesAutoresizingMaskIntoConstraints = false
|
|
655
|
|
- titleRowSpacer.setContentHuggingPriority(NSLayoutConstraint.Priority(1), for: .horizontal)
|
|
656
|
|
- titleRowSpacer.setContentCompressionResistancePriority(NSLayoutConstraint.Priority(1), for: .horizontal)
|
|
657
|
|
-
|
|
658
|
|
- let titleAndActionsRow = NSStackView(views: [titleField, titleRowSpacer, buttonRow])
|
|
659
|
|
- titleAndActionsRow.orientation = .horizontal
|
|
660
|
|
- titleAndActionsRow.spacing = 0
|
|
661
|
|
- titleAndActionsRow.setCustomSpacing(14, after: titleRowSpacer)
|
|
662
|
|
- titleAndActionsRow.alignment = .centerY
|
|
663
|
|
- titleAndActionsRow.distribution = .fill
|
|
664
|
|
- titleAndActionsRow.userInterfaceLayoutDirection = .leftToRight
|
|
665
|
|
- titleAndActionsRow.translatesAutoresizingMaskIntoConstraints = false
|
|
666
|
|
-
|
|
667
|
|
- let contentColumn = NSStackView(views: [titleAndActionsRow, descriptionField])
|
|
668
|
|
- contentColumn.orientation = .vertical
|
|
669
|
|
- contentColumn.spacing = 6
|
|
670
|
|
- contentColumn.alignment = .width
|
|
671
|
|
- contentColumn.translatesAutoresizingMaskIntoConstraints = false
|
|
672
|
|
-
|
|
673
|
|
- card.addSubview(contentColumn)
|
|
674
|
|
- NSLayoutConstraint.activate([
|
|
675
|
|
- contentColumn.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 16),
|
|
676
|
|
- contentColumn.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -16),
|
|
677
|
|
- contentColumn.topAnchor.constraint(equalTo: card.topAnchor, constant: 14),
|
|
678
|
|
- contentColumn.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -14),
|
|
679
|
|
-
|
|
680
|
|
- applyButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 72),
|
|
681
|
|
- applyButton.heightAnchor.constraint(equalToConstant: 28),
|
|
682
|
|
- savedButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 72),
|
|
683
|
|
- savedButton.heightAnchor.constraint(equalToConstant: 28),
|
|
684
|
|
- dismissButton.widthAnchor.constraint(equalToConstant: 28),
|
|
685
|
|
- dismissButton.heightAnchor.constraint(equalToConstant: 28),
|
|
686
|
|
-
|
|
687
|
|
- descriptionField.widthAnchor.constraint(equalTo: contentColumn.widthAnchor)
|
|
|
902
|
+ middleColumn.trailingAnchor.constraint(lessThanOrEqualTo: buttonRow.leadingAnchor, constant: -contentToActionsGap),
|
|
|
903
|
+
|
|
|
904
|
+ iconBox.widthAnchor.constraint(equalToConstant: 58),
|
|
|
905
|
+ iconBox.heightAnchor.constraint(equalToConstant: 58),
|
|
|
906
|
+
|
|
|
907
|
+ categoryIcon.centerXAnchor.constraint(equalTo: iconBox.centerXAnchor),
|
|
|
908
|
+ categoryIcon.centerYAnchor.constraint(equalTo: iconBox.centerYAnchor),
|
|
|
909
|
+
|
|
|
910
|
+ buildingIcon.widthAnchor.constraint(equalToConstant: 14),
|
|
|
911
|
+ buildingIcon.heightAnchor.constraint(equalToConstant: 14),
|
|
|
912
|
+
|
|
|
913
|
+ applyButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 76),
|
|
|
914
|
+ applyButton.heightAnchor.constraint(equalToConstant: 32),
|
|
|
915
|
+ savedButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 84),
|
|
|
916
|
+ savedButton.heightAnchor.constraint(equalToConstant: 32),
|
|
|
917
|
+ dismissButton.widthAnchor.constraint(equalToConstant: 32),
|
|
|
918
|
+ dismissButton.heightAnchor.constraint(equalToConstant: 32),
|
|
|
919
|
+
|
|
|
920
|
+ descriptionField.widthAnchor.constraint(equalTo: middleColumn.widthAnchor)
|
|
688
|
921
|
])
|
|
689
|
922
|
|
|
690
|
923
|
return card
|
|
|
@@ -692,7 +925,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
692
|
925
|
|
|
693
|
926
|
private func styleJobSavedButton(_ button: NSButton) {
|
|
694
|
927
|
button.wantsLayer = true
|
|
695
|
|
- button.layer?.cornerRadius = 6
|
|
|
928
|
+ button.layer?.cornerRadius = 8
|
|
696
|
929
|
let on = button.state == .on
|
|
697
|
930
|
let hovering = (button as? HoverableButton)?.isHovering ?? false
|
|
698
|
931
|
if on {
|
|
|
@@ -701,10 +934,10 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
701
|
934
|
button.layer?.borderColor = Theme.brandBlue.cgColor
|
|
702
|
935
|
button.contentTintColor = Theme.brandBlue
|
|
703
|
936
|
} else {
|
|
704
|
|
- button.layer?.backgroundColor = (hovering ? Theme.neutralHoverFill : Theme.cardBackground).cgColor
|
|
|
937
|
+ button.layer?.backgroundColor = (hovering ? Theme.proCardFill : Theme.cardBackground).cgColor
|
|
705
|
938
|
button.layer?.borderWidth = 1
|
|
706
|
|
- button.layer?.borderColor = Theme.border.cgColor
|
|
707
|
|
- button.contentTintColor = Theme.primaryText
|
|
|
939
|
+ button.layer?.borderColor = Theme.brandBlue.cgColor
|
|
|
940
|
+ button.contentTintColor = Theme.brandBlue
|
|
708
|
941
|
}
|
|
709
|
942
|
}
|
|
710
|
943
|
|
|
|
@@ -726,6 +959,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
726
|
959
|
applySavedState(willSave, for: job)
|
|
727
|
960
|
sender.state = willSave ? .on : .off
|
|
728
|
961
|
sender.title = willSave ? "Saved" : "Save"
|
|
|
962
|
+ sender.image = NSImage(systemSymbolName: willSave ? "heart.fill" : "heart", accessibilityDescription: nil)
|
|
729
|
963
|
styleJobSavedButton(sender)
|
|
730
|
964
|
if isSavedJobsSidebarIndex(selectedSidebarIndex) {
|
|
731
|
965
|
reloadSavedJobsListings()
|
|
|
@@ -819,9 +1053,9 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
819
|
1053
|
}
|
|
820
|
1054
|
|
|
821
|
1055
|
jobSearchIcon.translatesAutoresizingMaskIntoConstraints = false
|
|
822
|
|
- jobSearchIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 15, weight: .medium)
|
|
823
|
|
- jobSearchIcon.image = NSImage(systemSymbolName: "magnifyingglass", accessibilityDescription: "Job search")
|
|
824
|
|
- jobSearchIcon.contentTintColor = Theme.primaryText
|
|
|
1056
|
+ jobSearchIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 16, weight: .semibold)
|
|
|
1057
|
+ jobSearchIcon.image = NSImage(systemSymbolName: "sparkles", accessibilityDescription: "Ask AI")
|
|
|
1058
|
+ jobSearchIcon.contentTintColor = Theme.brandBlue
|
|
825
|
1059
|
|
|
826
|
1060
|
configureField(jobKeywordsField, placeholder: "Ask for roles, skills, salary, or job descriptions...")
|
|
827
|
1061
|
|
|
|
@@ -865,14 +1099,18 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
865
|
1099
|
|
|
866
|
1100
|
findJobsButton.translatesAutoresizingMaskIntoConstraints = false
|
|
867
|
1101
|
findJobsButton.title = ""
|
|
|
1102
|
+ findJobsButton.image = NSImage(systemSymbolName: "paperplane.fill", accessibilityDescription: nil)
|
|
|
1103
|
+ findJobsButton.imagePosition = .imageLeading
|
|
|
1104
|
+ findJobsButton.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 11, weight: .semibold)
|
|
868
|
1105
|
findJobsButton.attributedTitle = NSAttributedString(
|
|
869
|
|
- string: "Send",
|
|
|
1106
|
+ string: " Send",
|
|
870
|
1107
|
attributes: [
|
|
871
|
1108
|
.font: NSFont.systemFont(ofSize: 14, weight: .semibold),
|
|
872
|
1109
|
.foregroundColor: Theme.proCTAText,
|
|
873
|
1110
|
.kern: 0.35
|
|
874
|
1111
|
]
|
|
875
|
1112
|
)
|
|
|
1113
|
+ findJobsButton.contentTintColor = Theme.proCTAText
|
|
876
|
1114
|
findJobsButton.isBordered = false
|
|
877
|
1115
|
findJobsButton.bezelStyle = .rounded
|
|
878
|
1116
|
findJobsButton.wantsLayer = true
|
|
|
@@ -1324,6 +1562,26 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
1324
|
1562
|
window?.makeFirstResponder(nil)
|
|
1325
|
1563
|
}
|
|
1326
|
1564
|
|
|
|
1565
|
+ @objc private func didTapFeatureRole() {
|
|
|
1566
|
+ focusSearchField(seed: "Find roles similar to: ")
|
|
|
1567
|
+ }
|
|
|
1568
|
+
|
|
|
1569
|
+ @objc private func didTapFeatureCompany() {
|
|
|
1570
|
+ focusSearchField(seed: "Find jobs at company: ")
|
|
|
1571
|
+ }
|
|
|
1572
|
+
|
|
|
1573
|
+ @objc private func didTapFeatureSkill() {
|
|
|
1574
|
+ focusSearchField(seed: "Find jobs that require skill: ")
|
|
|
1575
|
+ }
|
|
|
1576
|
+
|
|
|
1577
|
+ private func focusSearchField(seed: String) {
|
|
|
1578
|
+ jobKeywordsField.stringValue = seed
|
|
|
1579
|
+ window?.makeFirstResponder(jobKeywordsField)
|
|
|
1580
|
+ if let editor = jobKeywordsField.window?.fieldEditor(true, for: jobKeywordsField) as? NSTextView {
|
|
|
1581
|
+ editor.moveToEndOfDocument(nil)
|
|
|
1582
|
+ }
|
|
|
1583
|
+ }
|
|
|
1584
|
+
|
|
1327
|
1585
|
@objc private func didTapLoadMoreJobs() {
|
|
1328
|
1586
|
let prompt = "Show more jobs"
|
|
1329
|
1587
|
guard !isAwaitingResponse, isContinuationPrompt(prompt) else { return }
|
|
|
@@ -1364,7 +1622,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
1364
|
1622
|
)
|
|
1365
|
1623
|
self.chatMessages.append(ChatMessage(role: "assistant", content: reply))
|
|
1366
|
1624
|
self.appendChatBubble(text: reply, isUser: false, jobs: freshJobs)
|
|
1367
|
|
- self.setChatStatusLabel("Ask for another role, company, or skill match")
|
|
|
1625
|
+ self.setChatStatusLabel(reply)
|
|
1368
|
1626
|
case .failure(let error):
|
|
1369
|
1627
|
self.appendChatBubble(text: error.localizedDescription, isUser: false)
|
|
1370
|
1628
|
if error is URLError {
|
|
|
@@ -2289,6 +2547,132 @@ private struct OpenAIAPIErrorResponse: Codable {
|
|
2289
|
2547
|
}
|
|
2290
|
2548
|
}
|
|
2291
|
2549
|
|
|
|
2550
|
+/// Home welcome row: three tappable shortcuts that seed the main search field (matches the reference dashboard tiles).
|
|
|
2551
|
+private final class FeatureShortcutCardView: NSView {
|
|
|
2552
|
+ private weak var actionTarget: AnyObject?
|
|
|
2553
|
+ private var actionSelector: Selector
|
|
|
2554
|
+
|
|
|
2555
|
+ init(symbolName: String, title: String, subtitle: String, target: AnyObject?, action: Selector) {
|
|
|
2556
|
+ self.actionTarget = target
|
|
|
2557
|
+ self.actionSelector = action
|
|
|
2558
|
+ super.init(frame: .zero)
|
|
|
2559
|
+ translatesAutoresizingMaskIntoConstraints = false
|
|
|
2560
|
+ wantsLayer = true
|
|
|
2561
|
+ layer?.cornerRadius = 14
|
|
|
2562
|
+ if #available(macOS 11.0, *) {
|
|
|
2563
|
+ layer?.cornerCurve = .continuous
|
|
|
2564
|
+ }
|
|
|
2565
|
+ layer?.backgroundColor = NSColor.white.cgColor
|
|
|
2566
|
+ layer?.masksToBounds = false
|
|
|
2567
|
+ layer?.shadowColor = NSColor.black.withAlphaComponent(0.12).cgColor
|
|
|
2568
|
+ layer?.shadowOffset = CGSize(width: 0, height: 2)
|
|
|
2569
|
+ layer?.shadowRadius = 10
|
|
|
2570
|
+ layer?.shadowOpacity = 1
|
|
|
2571
|
+
|
|
|
2572
|
+ let brandBlue = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 1)
|
|
|
2573
|
+ let iconWellColor = NSColor(srgbRed: 220 / 255, green: 235 / 255, blue: 252 / 255, alpha: 1)
|
|
|
2574
|
+ let secondary = NSColor(srgbRed: 118 / 255, green: 118 / 255, blue: 118 / 255, alpha: 1)
|
|
|
2575
|
+
|
|
|
2576
|
+ let iconHost = NSView()
|
|
|
2577
|
+ iconHost.translatesAutoresizingMaskIntoConstraints = false
|
|
|
2578
|
+ iconHost.wantsLayer = true
|
|
|
2579
|
+ iconHost.layer?.backgroundColor = iconWellColor.cgColor
|
|
|
2580
|
+ iconHost.layer?.cornerRadius = 22
|
|
|
2581
|
+
|
|
|
2582
|
+ let icon = NSImageView()
|
|
|
2583
|
+ icon.translatesAutoresizingMaskIntoConstraints = false
|
|
|
2584
|
+ icon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 16, weight: .semibold)
|
|
|
2585
|
+ icon.image = NSImage(systemSymbolName: symbolName, accessibilityDescription: nil)
|
|
|
2586
|
+ icon.contentTintColor = brandBlue
|
|
|
2587
|
+ iconHost.addSubview(icon)
|
|
|
2588
|
+
|
|
|
2589
|
+ let titleField = NSTextField(wrappingLabelWithString: title)
|
|
|
2590
|
+ titleField.font = .systemFont(ofSize: 15, weight: .bold)
|
|
|
2591
|
+ titleField.textColor = brandBlue
|
|
|
2592
|
+ titleField.maximumNumberOfLines = 1
|
|
|
2593
|
+ titleField.isEditable = false
|
|
|
2594
|
+ titleField.isBordered = false
|
|
|
2595
|
+ titleField.drawsBackground = false
|
|
|
2596
|
+ titleField.alignment = .left
|
|
|
2597
|
+
|
|
|
2598
|
+ let subtitleField = NSTextField(wrappingLabelWithString: subtitle)
|
|
|
2599
|
+ subtitleField.font = .systemFont(ofSize: 11, weight: .regular)
|
|
|
2600
|
+ subtitleField.textColor = secondary
|
|
|
2601
|
+ subtitleField.maximumNumberOfLines = 2
|
|
|
2602
|
+ subtitleField.isEditable = false
|
|
|
2603
|
+ subtitleField.isBordered = false
|
|
|
2604
|
+ subtitleField.drawsBackground = false
|
|
|
2605
|
+ subtitleField.alignment = .left
|
|
|
2606
|
+ subtitleField.preferredMaxLayoutWidth = 160
|
|
|
2607
|
+
|
|
|
2608
|
+ let textColumn = NSStackView(views: [titleField, subtitleField])
|
|
|
2609
|
+ textColumn.orientation = .vertical
|
|
|
2610
|
+ textColumn.spacing = 4
|
|
|
2611
|
+ textColumn.alignment = .leading
|
|
|
2612
|
+ textColumn.translatesAutoresizingMaskIntoConstraints = false
|
|
|
2613
|
+ textColumn.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
2614
|
+ textColumn.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
|
2615
|
+
|
|
|
2616
|
+ let chevron = NSImageView()
|
|
|
2617
|
+ chevron.translatesAutoresizingMaskIntoConstraints = false
|
|
|
2618
|
+ chevron.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 13, weight: .semibold)
|
|
|
2619
|
+ chevron.image = NSImage(systemSymbolName: "arrow.right", accessibilityDescription: nil)
|
|
|
2620
|
+ chevron.contentTintColor = brandBlue
|
|
|
2621
|
+ chevron.setContentHuggingPriority(.required, for: .horizontal)
|
|
|
2622
|
+ chevron.setContentCompressionResistancePriority(.required, for: .horizontal)
|
|
|
2623
|
+
|
|
|
2624
|
+ iconHost.setContentHuggingPriority(.required, for: .horizontal)
|
|
|
2625
|
+ iconHost.setContentCompressionResistancePriority(.required, for: .horizontal)
|
|
|
2626
|
+
|
|
|
2627
|
+ let row = NSStackView(views: [iconHost, textColumn, chevron])
|
|
|
2628
|
+ row.orientation = .horizontal
|
|
|
2629
|
+ row.spacing = 12
|
|
|
2630
|
+ row.alignment = .centerY
|
|
|
2631
|
+ row.distribution = .fill
|
|
|
2632
|
+ row.translatesAutoresizingMaskIntoConstraints = false
|
|
|
2633
|
+ addSubview(row)
|
|
|
2634
|
+
|
|
|
2635
|
+ NSLayoutConstraint.activate([
|
|
|
2636
|
+ row.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 14),
|
|
|
2637
|
+ row.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -14),
|
|
|
2638
|
+ row.topAnchor.constraint(equalTo: topAnchor, constant: 16),
|
|
|
2639
|
+ row.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -16),
|
|
|
2640
|
+
|
|
|
2641
|
+ iconHost.widthAnchor.constraint(equalToConstant: 44),
|
|
|
2642
|
+ iconHost.heightAnchor.constraint(equalToConstant: 44),
|
|
|
2643
|
+ icon.centerXAnchor.constraint(equalTo: iconHost.centerXAnchor),
|
|
|
2644
|
+ icon.centerYAnchor.constraint(equalTo: iconHost.centerYAnchor)
|
|
|
2645
|
+ ])
|
|
|
2646
|
+
|
|
|
2647
|
+ setAccessibilityElement(true)
|
|
|
2648
|
+ setAccessibilityRole(.button)
|
|
|
2649
|
+ setAccessibilityLabel("\(title). \(subtitle)")
|
|
|
2650
|
+ }
|
|
|
2651
|
+
|
|
|
2652
|
+ @available(*, unavailable)
|
|
|
2653
|
+ required init?(coder: NSCoder) {
|
|
|
2654
|
+ fatalError("init(coder:) has not been implemented")
|
|
|
2655
|
+ }
|
|
|
2656
|
+
|
|
|
2657
|
+ override func layout() {
|
|
|
2658
|
+ super.layout()
|
|
|
2659
|
+ guard let layer = layer, bounds.width > 0, bounds.height > 0 else { return }
|
|
|
2660
|
+ let r = bounds
|
|
|
2661
|
+ layer.shadowPath = CGPath(roundedRect: r, cornerWidth: 14, cornerHeight: 14, transform: nil)
|
|
|
2662
|
+ }
|
|
|
2663
|
+
|
|
|
2664
|
+ override func mouseDown(with event: NSEvent) {
|
|
|
2665
|
+ if let target = actionTarget {
|
|
|
2666
|
+ _ = target.perform(actionSelector, with: nil)
|
|
|
2667
|
+ }
|
|
|
2668
|
+ }
|
|
|
2669
|
+
|
|
|
2670
|
+ override func resetCursorRects() {
|
|
|
2671
|
+ super.resetCursorRects()
|
|
|
2672
|
+ addCursorRect(bounds, cursor: .pointingHand)
|
|
|
2673
|
+ }
|
|
|
2674
|
+}
|
|
|
2675
|
+
|
|
2292
|
2676
|
/// `NSButton` that carries a `JobListing` for card actions (`representedObject` is unavailable on `NSButton` in this target).
|
|
2293
|
2677
|
private final class JobPayloadButton: HoverableButton {
|
|
2294
|
2678
|
var jobPayload: JobListing?
|