Ver Fonte

Refine dashboard home UI: status banner, feature cards, and theme colors

Align welcome hero, search bar, and chat status strip with the reference layout.
Add feature shortcut cards (Role, Company, etc.), status branding row, and
adjust subtitle and border colors for readability.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 há 2 meses atrás
pai
commit
8a252e60bf
1 ficheiros alterados com 464 adições e 80 exclusões
  1. 464 80
      App for Indeed/Views/DashboardView.swift

+ 464 - 80
App for Indeed/Views/DashboardView.swift

@@ -20,8 +20,8 @@ final class DashboardView: NSView, NSTextFieldDelegate {
         static let chromeBackground = NSColor(srgbRed: 247 / 255, green: 247 / 255, blue: 247 / 255, alpha: 1)
         static let sidebarBackground = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
         static let mainHostBackground = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
-        /// Subtitle on the welcome hero: readable blue-gray aligned with brand.
-        static let welcomeSubtitleText = NSColor(srgbRed: 52 / 255, green: 92 / 255, blue: 142 / 255, alpha: 1)
+        /// Subtitle on the welcome hero: dark neutral gray to match the reference layout.
+        static let welcomeSubtitleText = NSColor(srgbRed: 64 / 255, green: 64 / 255, blue: 64 / 255, alpha: 1)
         static let selectionFill = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 0.12)
         static let cardBackground = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
         static let toggleBackground = NSColor(srgbRed: 232 / 255, green: 232 / 255, blue: 232 / 255, alpha: 1)
@@ -29,10 +29,13 @@ final class DashboardView: NSView, NSTextFieldDelegate {
         static let secondaryText = NSColor(srgbRed: 118 / 255, green: 118 / 255, blue: 118 / 255, alpha: 1)
         static let tertiaryText = NSColor(srgbRed: 118 / 255, green: 118 / 255, blue: 118 / 255, alpha: 1)
         static let border = NSColor(srgbRed: 212 / 255, green: 210 / 255, blue: 208 / 255, alpha: 1)
-        /// Job search bar outer stroke (charcoal).
-        static let searchBarBorder = NSColor(srgbRed: 58 / 255, green: 58 / 255, blue: 58 / 255, alpha: 1)
+        /// Home status strip and soft accents (light blue panel in the reference UI).
+        static let statusBannerBackground = NSColor(srgbRed: 232 / 255, green: 242 / 255, blue: 252 / 255, alpha: 1)
+        static let featureIconWell = NSColor(srgbRed: 220 / 255, green: 235 / 255, blue: 252 / 255, alpha: 1)
+        /// Job search bar outer stroke (soft blue-gray, pill field in the reference UI).
+        static let searchBarBorder = NSColor(srgbRed: 180 / 255, green: 200 / 255, blue: 228 / 255, alpha: 1)
         /// Search bar border on hover (brand-tinted, matches focus affordance elsewhere).
-        static let searchBarBorderHover = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 0.45)
+        static let searchBarBorderHover = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 0.55)
         static let proCardFill = NSColor(srgbRed: 239 / 255, green: 244 / 255, blue: 252 / 255, alpha: 1)
         static let proCardBorder = NSColor(srgbRed: 212 / 255, green: 210 / 255, blue: 208 / 255, alpha: 1)
         static let proAccent = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 1)
@@ -81,11 +84,18 @@ final class DashboardView: NSView, NSTextFieldDelegate {
     private let findJobsCTAHost = NSView()
     private let findJobsCTAChrome = HoverableView()
     private var findJobsCTAGradientLayer: CAGradientLayer?
-    private let chatStatusStack = NSStackView()
+    private let statusBannerContainer = NSView()
+    private let statusBannerInner = NSView()
+    private let statusBannerRow = NSStackView()
     private let chatStatusSymbolContainer = NSView()
     private let chatStatusIcon = NSImageView()
     private let chatStatusLoadingIndicator = NSProgressIndicator()
-    private let chatStatusLabel = NSTextField(labelWithString: "Opening the vault...")
+    private let chatStatusLabel = NSTextField(wrappingLabelWithString: "Opening the vault...")
+    private let statusBrandTrailing = NSStackView()
+    private let statusBrandStarIcon = NSImageView()
+    private let statusBrandLabel = NSTextField(labelWithString: "AI Job Finder")
+    private let welcomeSparkleIcon = NSImageView()
+    private let featureCardsRow = NSStackView()
     private let chatScrollView = NSScrollView()
     private let chatDocumentView = JobListingsDocumentView()
     private let chatStack = NSStackView()
@@ -145,6 +155,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
         updateFindJobsCTAShadowPath()
         updateJobListingDescriptionWidths()
         updateChatBubbleWidths()
+        updateStatusBannerLabelWidth()
     }
 
     func render(_ data: DashboardData) {
@@ -226,14 +237,21 @@ final class DashboardView: NSView, NSTextFieldDelegate {
         configureSearchBar()
         configureChatViews()
 
-        let titleBlock = NSStackView(views: [greetingLabel, subtitleLabel])
+        welcomeSparkleIcon.translatesAutoresizingMaskIntoConstraints = false
+        welcomeSparkleIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 16, weight: .semibold)
+        welcomeSparkleIcon.image = NSImage(systemSymbolName: "sparkles", accessibilityDescription: nil)
+        welcomeSparkleIcon.contentTintColor = Theme.brandBlue
+
+        let titleBlock = NSStackView(views: [greetingLabel, subtitleLabel, welcomeSparkleIcon])
         titleBlock.orientation = .vertical
         titleBlock.spacing = 10
         titleBlock.alignment = .centerX
 
+        configureFeatureShortcutCards()
+
         let midSpacer = NSView()
         midSpacer.translatesAutoresizingMaskIntoConstraints = false
-        midSpacer.heightAnchor.constraint(equalToConstant: 18).isActive = true
+        midSpacer.heightAnchor.constraint(equalToConstant: 12).isActive = true
 
         let chatTopSpacer = NSView()
         chatTopSpacer.translatesAutoresizingMaskIntoConstraints = false
@@ -245,8 +263,9 @@ final class DashboardView: NSView, NSTextFieldDelegate {
 
         mainOverlay.addArrangedSubview(topInset)
         mainOverlay.addArrangedSubview(titleBlock)
+        mainOverlay.addArrangedSubview(featureCardsRow)
         mainOverlay.addArrangedSubview(midSpacer)
-        mainOverlay.addArrangedSubview(chatStatusStack)
+        mainOverlay.addArrangedSubview(statusBannerContainer)
         mainOverlay.addArrangedSubview(chatTopSpacer)
         mainOverlay.addArrangedSubview(chatScrollView)
         mainOverlay.addArrangedSubview(chatBottomSpacer)
@@ -280,7 +299,8 @@ final class DashboardView: NSView, NSTextFieldDelegate {
             nonHomeHost.bottomAnchor.constraint(equalTo: mainHost.bottomAnchor, constant: -24),
 
             searchBarShadowHost.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
-            chatStatusStack.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
+            statusBannerContainer.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
+            featureCardsRow.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
             chatScrollView.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
 
             greetingLabel.leadingAnchor.constraint(equalTo: mainOverlay.leadingAnchor, constant: 16),
@@ -290,19 +310,60 @@ final class DashboardView: NSView, NSTextFieldDelegate {
         ])
     }
 
+    private func configureFeatureShortcutCards() {
+        featureCardsRow.orientation = .horizontal
+        featureCardsRow.spacing = 12
+        featureCardsRow.distribution = .fillEqually
+        featureCardsRow.alignment = .top
+        featureCardsRow.translatesAutoresizingMaskIntoConstraints = false
+
+        let specs: [(symbol: String, title: String, subtitle: String, action: Selector)] = [
+            ("briefcase.fill", "Role", "Explore similar or better job roles", #selector(didTapFeatureRole)),
+            ("building.2.fill", "Company", "Find opportunities at other companies", #selector(didTapFeatureCompany)),
+            ("chevron.left.forwardslash.chevron.right", "Skill", "Match jobs that fit your skills", #selector(didTapFeatureSkill))
+        ]
+        for spec in specs {
+            let card = FeatureShortcutCardView(
+                symbolName: spec.symbol,
+                title: spec.title,
+                subtitle: spec.subtitle,
+                target: self,
+                action: spec.action
+            )
+            featureCardsRow.addArrangedSubview(card)
+        }
+    }
+
     private func configureChatViews() {
-        chatStatusStack.orientation = .vertical
-        chatStatusStack.spacing = 6
-        chatStatusStack.alignment = .centerX
-        chatStatusStack.translatesAutoresizingMaskIntoConstraints = false
+        statusBannerContainer.translatesAutoresizingMaskIntoConstraints = false
+        statusBannerContainer.setContentHuggingPriority(.defaultHigh, for: .vertical)
+
+        statusBannerInner.translatesAutoresizingMaskIntoConstraints = false
+        statusBannerInner.wantsLayer = true
+        statusBannerInner.layer?.backgroundColor = Theme.statusBannerBackground.cgColor
+        statusBannerInner.layer?.cornerRadius = 14
+        if #available(macOS 11.0, *) {
+            statusBannerInner.layer?.cornerCurve = .continuous
+        }
+        statusBannerInner.layer?.masksToBounds = true
+
+        statusBannerContainer.addSubview(statusBannerInner)
+
+        statusBannerRow.orientation = .horizontal
+        statusBannerRow.spacing = 12
+        statusBannerRow.alignment = .top
+        statusBannerRow.translatesAutoresizingMaskIntoConstraints = false
 
         chatStatusSymbolContainer.translatesAutoresizingMaskIntoConstraints = false
+        chatStatusSymbolContainer.wantsLayer = true
+        chatStatusSymbolContainer.layer?.backgroundColor = Theme.brandBlue.cgColor
+        chatStatusSymbolContainer.layer?.cornerRadius = 20
 
         chatStatusIcon.translatesAutoresizingMaskIntoConstraints = false
         chatStatusIcon.wantsLayer = true
-        chatStatusIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 36, weight: .regular)
+        chatStatusIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 15, weight: .semibold)
         chatStatusIcon.image = NSImage(systemSymbolName: "sparkles", accessibilityDescription: "Assistant status")
-        chatStatusIcon.contentTintColor = Theme.brandBlue
+        chatStatusIcon.contentTintColor = .white
 
         chatStatusLoadingIndicator.translatesAutoresizingMaskIntoConstraints = false
         chatStatusLoadingIndicator.style = .spinning
@@ -323,19 +384,66 @@ final class DashboardView: NSView, NSTextFieldDelegate {
             chatStatusIcon.centerYAnchor.constraint(equalTo: chatStatusSymbolContainer.centerYAnchor),
             chatStatusLoadingIndicator.centerXAnchor.constraint(equalTo: chatStatusSymbolContainer.centerXAnchor),
             chatStatusLoadingIndicator.centerYAnchor.constraint(equalTo: chatStatusSymbolContainer.centerYAnchor),
-            chatStatusLoadingIndicator.widthAnchor.constraint(equalToConstant: 32),
-            chatStatusLoadingIndicator.heightAnchor.constraint(equalToConstant: 32)
+            chatStatusLoadingIndicator.widthAnchor.constraint(equalToConstant: 26),
+            chatStatusLoadingIndicator.heightAnchor.constraint(equalToConstant: 26)
         ])
 
-        chatStatusLabel.font = .systemFont(ofSize: 20, weight: .semibold)
+        chatStatusLabel.font = .systemFont(ofSize: 13, weight: .regular)
         chatStatusLabel.textColor = Theme.primaryText
-        chatStatusLabel.alignment = .center
-        chatStatusLabel.maximumNumberOfLines = 1
+        chatStatusLabel.alignment = .left
+        chatStatusLabel.maximumNumberOfLines = 0
+        chatStatusLabel.lineBreakMode = .byWordWrapping
+        chatStatusLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
+        chatStatusLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
+        chatStatusLabel.translatesAutoresizingMaskIntoConstraints = false
+
+        statusBrandStarIcon.translatesAutoresizingMaskIntoConstraints = false
+        statusBrandStarIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 12, weight: .semibold)
+        statusBrandStarIcon.image = NSImage(systemSymbolName: "sparkle", accessibilityDescription: nil)
+        statusBrandStarIcon.contentTintColor = Theme.brandBlue
+
+        statusBrandLabel.font = .systemFont(ofSize: 12, weight: .semibold)
+        statusBrandLabel.textColor = Theme.brandBlue
+        statusBrandLabel.alignment = .right
+        statusBrandLabel.maximumNumberOfLines = 1
+
+        statusBrandTrailing.orientation = .horizontal
+        statusBrandTrailing.spacing = 5
+        statusBrandTrailing.alignment = .centerY
+        statusBrandTrailing.translatesAutoresizingMaskIntoConstraints = false
+        statusBrandTrailing.addArrangedSubview(statusBrandStarIcon)
+        statusBrandTrailing.addArrangedSubview(statusBrandLabel)
+        statusBrandTrailing.setContentHuggingPriority(.required, for: .horizontal)
+        statusBrandTrailing.setContentCompressionResistancePriority(.required, for: .horizontal)
+
+        statusBannerRow.addArrangedSubview(chatStatusSymbolContainer)
+        statusBannerRow.addArrangedSubview(chatStatusLabel)
+
+        statusBannerInner.addSubview(statusBannerRow)
+        statusBannerContainer.addSubview(statusBannerInner)
+        statusBannerContainer.addSubview(statusBrandTrailing)
+        statusBannerInner.setContentHuggingPriority(.defaultHigh, for: .horizontal)
+        statusBannerInner.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
 
-        chatStatusStack.addArrangedSubview(chatStatusSymbolContainer)
-        chatStatusStack.addArrangedSubview(chatStatusLabel)
+        NSLayoutConstraint.activate([
+            statusBannerInner.leadingAnchor.constraint(equalTo: statusBannerContainer.leadingAnchor),
+            statusBannerInner.topAnchor.constraint(equalTo: statusBannerContainer.topAnchor),
+            statusBannerInner.bottomAnchor.constraint(equalTo: statusBannerContainer.bottomAnchor),
+            statusBannerInner.trailingAnchor.constraint(lessThanOrEqualTo: statusBrandTrailing.leadingAnchor, constant: -16),
+
+            statusBrandTrailing.trailingAnchor.constraint(equalTo: statusBannerContainer.trailingAnchor),
+            statusBrandTrailing.centerYAnchor.constraint(equalTo: statusBannerContainer.centerYAnchor),
+
+            statusBannerRow.leadingAnchor.constraint(equalTo: statusBannerInner.leadingAnchor, constant: 14),
+            statusBannerRow.trailingAnchor.constraint(equalTo: statusBannerInner.trailingAnchor, constant: -16),
+            statusBannerRow.topAnchor.constraint(equalTo: statusBannerInner.topAnchor, constant: 10),
+            statusBannerRow.bottomAnchor.constraint(equalTo: statusBannerInner.bottomAnchor, constant: -10),
+
+            statusBannerContainer.heightAnchor.constraint(greaterThanOrEqualToConstant: 52)
+        ])
 
         syncChatStatusLoadingIndicator(forStatusText: chatStatusLabel.stringValue)
+        syncChatStatusBannerVisuals(forStatusText: chatStatusLabel.stringValue)
 
         chatDocumentView.translatesAutoresizingMaskIntoConstraints = false
         chatStack.orientation = .vertical
@@ -374,7 +482,43 @@ final class DashboardView: NSView, NSTextFieldDelegate {
     private func setChatStatusLabel(_ text: String) {
         chatStatusLabel.stringValue = text
         syncChatStatusLoadingIndicator(forStatusText: text)
+        syncChatStatusBannerVisuals(forStatusText: text)
         syncChatStatusSparkleAnimation()
+        updateStatusBannerLabelWidth()
+    }
+
+    private func updateStatusBannerLabelWidth() {
+        guard statusBannerContainer.bounds.width > 1 else { return }
+        let trailingWidth = max(96, statusBrandTrailing.fittingSize.width)
+        let chrome: CGFloat = 14 + 40 + 12 + 16 + 16 + trailingWidth
+        let target = max(80, statusBannerContainer.bounds.width - chrome)
+        if abs(chatStatusLabel.preferredMaxLayoutWidth - target) > 0.5 {
+            chatStatusLabel.preferredMaxLayoutWidth = target
+            chatStatusLabel.invalidateIntrinsicContentSize()
+        }
+    }
+
+    /// Leading status glyph: sparkles for prompts, magnifying glass for search-result summaries and errors.
+    private func syncChatStatusBannerVisuals(forStatusText text: String) {
+        if text == "Thinking..." { return }
+        let lower = text.lowercased()
+        let useMagnifyingGlass =
+            text.hasPrefix("Found ")
+            || text.hasPrefix("Here are")
+            || text.hasPrefix("No jobs")
+            || text.contains("couldn't find")
+            || lower.contains("try again")
+            || lower.contains("could not reach")
+            || lower.contains("search did not finish")
+        let config = NSImage.SymbolConfiguration(pointSize: 15, weight: .semibold)
+        chatStatusIcon.symbolConfiguration = config
+        if useMagnifyingGlass {
+            chatStatusIcon.image = NSImage(systemSymbolName: "magnifyingglass", accessibilityDescription: "Search status")
+        } else {
+            chatStatusIcon.image = NSImage(systemSymbolName: "sparkles", accessibilityDescription: "Assistant status")
+        }
+        chatStatusIcon.contentTintColor = .white
+        chatStatusSymbolContainer.layer?.backgroundColor = Theme.brandBlue.cgColor
     }
 
     private func syncChatStatusLoadingIndicator(forStatusText text: String) {
@@ -383,9 +527,12 @@ final class DashboardView: NSView, NSTextFieldDelegate {
             chatStatusIcon.isHidden = true
             chatStatusLoadingIndicator.isHidden = false
             chatStatusLoadingIndicator.startAnimation(nil)
+            chatStatusSymbolContainer.layer?.backgroundColor = Theme.featureIconWell.cgColor
         } else {
             chatStatusLoadingIndicator.stopAnimation(nil)
             chatStatusIcon.isHidden = false
+            chatStatusLoadingIndicator.isHidden = true
+            chatStatusSymbolContainer.layer?.backgroundColor = Theme.brandBlue.cgColor
         }
     }
 
@@ -399,7 +546,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
 
     private func shouldAnimateChatStatusSparkles(for statusText: String) -> Bool {
         statusText == "Ask me to find jobs"
-            || statusText == "Ask for another role, company, or skill match"
+            || statusText == "Opening the vault..."
     }
 
     private func syncChatStatusSparkleAnimation() {
@@ -542,30 +689,99 @@ final class DashboardView: NSView, NSTextFieldDelegate {
         persistSavedJobs()
     }
 
+    private func jobListingHostSubtitle(_ job: JobListing) -> String {
+        guard let raw = job.url, let url = URL(string: raw), let host = url.host?.lowercased() else {
+            return "Indeed"
+        }
+        if host.hasPrefix("www.") {
+            return String(host.dropFirst(4))
+        }
+        return host
+    }
+
+    private func jobListingCategorySymbol(for job: JobListing) -> String {
+        let blob = (job.title + " " + job.description).lowercased()
+        if blob.contains("machine learning") || blob.contains("deep learning") || blob.contains(" ml ") {
+            return "brain.head.profile"
+        }
+        if blob.contains("audio") || blob.contains(" sound ") || blob.contains("dsp") {
+            return "waveform"
+        }
+        if blob.contains("ios") || blob.contains("swift") || blob.contains("mobile") {
+            return "iphone"
+        }
+        if blob.contains("design") || blob.contains(" ux") || blob.contains("figma") {
+            return "paintpalette.fill"
+        }
+        if blob.contains("data ") || blob.contains("analytics") {
+            return "chart.bar.fill"
+        }
+        if blob.contains("ai") || blob.contains("llm") || blob.contains("nlp") {
+            return "cpu"
+        }
+        return "briefcase.fill"
+    }
+
     private func makeJobListingCard(_ job: JobListing, context: JobListingCardContext) -> NSView {
         let card = NSView()
         card.translatesAutoresizingMaskIntoConstraints = false
         card.wantsLayer = true
         card.layer?.backgroundColor = Theme.cardBackground.cgColor
-        card.layer?.cornerRadius = 12
+        card.layer?.cornerRadius = 14
         card.layer?.borderWidth = 1
         card.layer?.borderColor = Theme.border.cgColor
         card.layer?.masksToBounds = true
 
+        let iconBox = NSView()
+        iconBox.translatesAutoresizingMaskIntoConstraints = false
+        iconBox.wantsLayer = true
+        iconBox.layer?.backgroundColor = Theme.brandBlue.cgColor
+        iconBox.layer?.cornerRadius = 12
+        if #available(macOS 11.0, *) {
+            iconBox.layer?.cornerCurve = .continuous
+        }
+
+        let categoryIcon = NSImageView()
+        categoryIcon.translatesAutoresizingMaskIntoConstraints = false
+        categoryIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 22, weight: .medium)
+        categoryIcon.image = NSImage(systemSymbolName: jobListingCategorySymbol(for: job), accessibilityDescription: nil)
+        categoryIcon.contentTintColor = .white
+        iconBox.addSubview(categoryIcon)
+
         let titleField = NSTextField(labelWithString: job.title)
         titleField.font = .systemFont(ofSize: 16, weight: .semibold)
-        titleField.textColor = Theme.primaryText
+        titleField.textColor = Theme.brandBlue
         titleField.maximumNumberOfLines = 2
         titleField.lineBreakMode = .byWordWrapping
         titleField.alignment = .left
+        titleField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
         titleField.translatesAutoresizingMaskIntoConstraints = false
 
+        let buildingIcon = NSImageView()
+        buildingIcon.translatesAutoresizingMaskIntoConstraints = false
+        buildingIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 12, weight: .medium)
+        buildingIcon.image = NSImage(systemSymbolName: "building.2.fill", accessibilityDescription: nil)
+        buildingIcon.contentTintColor = Theme.welcomeSubtitleText
+
+        let companyLabel = NSTextField(labelWithString: jobListingHostSubtitle(job))
+        companyLabel.font = .systemFont(ofSize: 12, weight: .medium)
+        companyLabel.textColor = Theme.welcomeSubtitleText
+        companyLabel.maximumNumberOfLines = 1
+        companyLabel.lineBreakMode = .byTruncatingTail
+        companyLabel.translatesAutoresizingMaskIntoConstraints = false
+
+        let companyRow = NSStackView(views: [buildingIcon, companyLabel])
+        companyRow.orientation = .horizontal
+        companyRow.spacing = 5
+        companyRow.alignment = .centerY
+        companyRow.translatesAutoresizingMaskIntoConstraints = false
+
         let descriptionField = NSTextField(wrappingLabelWithString: job.description)
         descriptionField.font = .systemFont(ofSize: 13, weight: .regular)
         descriptionField.textColor = Theme.secondaryText
-        descriptionField.maximumNumberOfLines = 0
-        descriptionField.alignment = .left
+        descriptionField.maximumNumberOfLines = 2
         descriptionField.lineBreakMode = .byWordWrapping
+        descriptionField.alignment = .left
         descriptionField.baseWritingDirection = .leftToRight
         descriptionField.attributedStringValue = Self.jobListingDescriptionAttributedString(job.description)
         if let cell = descriptionField.cell as? NSTextFieldCell {
@@ -584,7 +800,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
         applyButton.bezelStyle = .rounded
         applyButton.font = .systemFont(ofSize: 13, weight: .semibold)
         applyButton.wantsLayer = true
-        applyButton.layer?.cornerRadius = 6
+        applyButton.layer?.cornerRadius = 8
         applyButton.layer?.backgroundColor = Theme.brandBlue.cgColor
         applyButton.contentTintColor = Theme.proCTAText
         applyButton.focusRingType = .none
@@ -603,6 +819,9 @@ final class DashboardView: NSView, NSTextFieldDelegate {
         savedButton.isBordered = false
         savedButton.bezelStyle = .rounded
         savedButton.font = .systemFont(ofSize: 13, weight: .semibold)
+        savedButton.image = NSImage(systemSymbolName: savedOn ? "heart.fill" : "heart", accessibilityDescription: nil)
+        savedButton.imagePosition = .imageLeading
+        savedButton.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 11, weight: .semibold)
         savedButton.focusRingType = .none
         savedButton.state = savedOn ? .on : .off
         savedButton.pointerCursor = true
@@ -620,7 +839,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
         dismissButton.image = NSImage(systemSymbolName: "xmark", accessibilityDescription: "Dismiss")
         dismissButton.imagePosition = .imageOnly
         dismissButton.imageScaling = .scaleProportionallyDown
-        dismissButton.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 11, weight: .semibold)
+        dismissButton.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 12, weight: .semibold)
         dismissButton.isBordered = false
         dismissButton.bezelStyle = .rounded
         dismissButton.contentTintColor = Theme.secondaryText
@@ -629,7 +848,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
         dismissButton.toolTip = context == .savedJobsPage ? "Remove from saved" : "Dismiss"
         dismissButton.focusRingType = .none
         dismissButton.wantsLayer = true
-        dismissButton.layer?.cornerRadius = 14
+        dismissButton.layer?.cornerRadius = 8
         dismissButton.layer?.backgroundColor = NSColor.clear.cgColor
         dismissButton.pointerCursor = true
         dismissButton.hoverHandler = { [weak dismissButton] hovering in
@@ -641,50 +860,64 @@ final class DashboardView: NSView, NSTextFieldDelegate {
         let buttonRow = NSStackView(views: [applyButton, savedButton, dismissButton])
         buttonRow.orientation = .horizontal
         buttonRow.spacing = 8
-        buttonRow.alignment = .centerY
+        buttonRow.alignment = .top
         buttonRow.translatesAutoresizingMaskIntoConstraints = false
         buttonRow.setContentHuggingPriority(.required, for: .horizontal)
         buttonRow.setContentCompressionResistancePriority(.required, for: .horizontal)
+        buttonRow.setContentHuggingPriority(.required, for: .vertical)
+        buttonRow.setContentCompressionResistancePriority(.required, for: .vertical)
+        applyButton.setContentCompressionResistancePriority(.required, for: .horizontal)
+        savedButton.setContentCompressionResistancePriority(.required, for: .horizontal)
+        dismissButton.setContentCompressionResistancePriority(.required, for: .horizontal)
+
+        let middleColumn = NSStackView(views: [titleField, companyRow, descriptionField])
+        middleColumn.orientation = .vertical
+        middleColumn.spacing = 5
+        middleColumn.alignment = .leading
+        middleColumn.translatesAutoresizingMaskIntoConstraints = false
+        middleColumn.setContentHuggingPriority(.defaultLow, for: .horizontal)
+        middleColumn.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
+
+        let contentRow = NSStackView(views: [iconBox, middleColumn])
+        contentRow.orientation = .horizontal
+        contentRow.spacing = 14
+        contentRow.alignment = .top
+        contentRow.distribution = .fill
+        contentRow.translatesAutoresizingMaskIntoConstraints = false
+
+        card.addSubview(contentRow)
+        card.addSubview(buttonRow)
+        let actionCornerInset: CGFloat = 8
+        let contentToActionsGap: CGFloat = 12
+        let bodyTrailingInset: CGFloat = 16
+        NSLayoutConstraint.activate([
+            contentRow.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 16),
+            contentRow.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -bodyTrailingInset),
+            contentRow.topAnchor.constraint(equalTo: card.topAnchor, constant: 14),
+            contentRow.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -14),
 
-        // Title hugs the leading edge; a low–hugging-priority spacer absorbs remaining width so buttons stay trailing.
-        titleField.setContentHuggingPriority(.defaultHigh, for: .horizontal)
-        titleField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
+            buttonRow.topAnchor.constraint(equalTo: card.topAnchor, constant: actionCornerInset),
+            buttonRow.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -actionCornerInset),
 
-        let titleRowSpacer = NSView()
-        titleRowSpacer.translatesAutoresizingMaskIntoConstraints = false
-        titleRowSpacer.setContentHuggingPriority(NSLayoutConstraint.Priority(1), for: .horizontal)
-        titleRowSpacer.setContentCompressionResistancePriority(NSLayoutConstraint.Priority(1), for: .horizontal)
-
-        let titleAndActionsRow = NSStackView(views: [titleField, titleRowSpacer, buttonRow])
-        titleAndActionsRow.orientation = .horizontal
-        titleAndActionsRow.spacing = 0
-        titleAndActionsRow.setCustomSpacing(14, after: titleRowSpacer)
-        titleAndActionsRow.alignment = .centerY
-        titleAndActionsRow.distribution = .fill
-        titleAndActionsRow.userInterfaceLayoutDirection = .leftToRight
-        titleAndActionsRow.translatesAutoresizingMaskIntoConstraints = false
-
-        let contentColumn = NSStackView(views: [titleAndActionsRow, descriptionField])
-        contentColumn.orientation = .vertical
-        contentColumn.spacing = 6
-        contentColumn.alignment = .width
-        contentColumn.translatesAutoresizingMaskIntoConstraints = false
-
-        card.addSubview(contentColumn)
-        NSLayoutConstraint.activate([
-            contentColumn.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 16),
-            contentColumn.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -16),
-            contentColumn.topAnchor.constraint(equalTo: card.topAnchor, constant: 14),
-            contentColumn.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -14),
-
-            applyButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 72),
-            applyButton.heightAnchor.constraint(equalToConstant: 28),
-            savedButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 72),
-            savedButton.heightAnchor.constraint(equalToConstant: 28),
-            dismissButton.widthAnchor.constraint(equalToConstant: 28),
-            dismissButton.heightAnchor.constraint(equalToConstant: 28),
-
-            descriptionField.widthAnchor.constraint(equalTo: contentColumn.widthAnchor)
+            middleColumn.trailingAnchor.constraint(lessThanOrEqualTo: buttonRow.leadingAnchor, constant: -contentToActionsGap),
+
+            iconBox.widthAnchor.constraint(equalToConstant: 58),
+            iconBox.heightAnchor.constraint(equalToConstant: 58),
+
+            categoryIcon.centerXAnchor.constraint(equalTo: iconBox.centerXAnchor),
+            categoryIcon.centerYAnchor.constraint(equalTo: iconBox.centerYAnchor),
+
+            buildingIcon.widthAnchor.constraint(equalToConstant: 14),
+            buildingIcon.heightAnchor.constraint(equalToConstant: 14),
+
+            applyButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 76),
+            applyButton.heightAnchor.constraint(equalToConstant: 32),
+            savedButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 84),
+            savedButton.heightAnchor.constraint(equalToConstant: 32),
+            dismissButton.widthAnchor.constraint(equalToConstant: 32),
+            dismissButton.heightAnchor.constraint(equalToConstant: 32),
+
+            descriptionField.widthAnchor.constraint(equalTo: middleColumn.widthAnchor)
         ])
 
         return card
@@ -692,7 +925,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
 
     private func styleJobSavedButton(_ button: NSButton) {
         button.wantsLayer = true
-        button.layer?.cornerRadius = 6
+        button.layer?.cornerRadius = 8
         let on = button.state == .on
         let hovering = (button as? HoverableButton)?.isHovering ?? false
         if on {
@@ -701,10 +934,10 @@ final class DashboardView: NSView, NSTextFieldDelegate {
             button.layer?.borderColor = Theme.brandBlue.cgColor
             button.contentTintColor = Theme.brandBlue
         } else {
-            button.layer?.backgroundColor = (hovering ? Theme.neutralHoverFill : Theme.cardBackground).cgColor
+            button.layer?.backgroundColor = (hovering ? Theme.proCardFill : Theme.cardBackground).cgColor
             button.layer?.borderWidth = 1
-            button.layer?.borderColor = Theme.border.cgColor
-            button.contentTintColor = Theme.primaryText
+            button.layer?.borderColor = Theme.brandBlue.cgColor
+            button.contentTintColor = Theme.brandBlue
         }
     }
 
@@ -726,6 +959,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
         applySavedState(willSave, for: job)
         sender.state = willSave ? .on : .off
         sender.title = willSave ? "Saved" : "Save"
+        sender.image = NSImage(systemSymbolName: willSave ? "heart.fill" : "heart", accessibilityDescription: nil)
         styleJobSavedButton(sender)
         if isSavedJobsSidebarIndex(selectedSidebarIndex) {
             reloadSavedJobsListings()
@@ -819,9 +1053,9 @@ final class DashboardView: NSView, NSTextFieldDelegate {
         }
 
         jobSearchIcon.translatesAutoresizingMaskIntoConstraints = false
-        jobSearchIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 15, weight: .medium)
-        jobSearchIcon.image = NSImage(systemSymbolName: "magnifyingglass", accessibilityDescription: "Job search")
-        jobSearchIcon.contentTintColor = Theme.primaryText
+        jobSearchIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 16, weight: .semibold)
+        jobSearchIcon.image = NSImage(systemSymbolName: "sparkles", accessibilityDescription: "Ask AI")
+        jobSearchIcon.contentTintColor = Theme.brandBlue
 
         configureField(jobKeywordsField, placeholder: "Ask for roles, skills, salary, or job descriptions...")
 
@@ -865,14 +1099,18 @@ final class DashboardView: NSView, NSTextFieldDelegate {
 
         findJobsButton.translatesAutoresizingMaskIntoConstraints = false
         findJobsButton.title = ""
+        findJobsButton.image = NSImage(systemSymbolName: "paperplane.fill", accessibilityDescription: nil)
+        findJobsButton.imagePosition = .imageLeading
+        findJobsButton.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 11, weight: .semibold)
         findJobsButton.attributedTitle = NSAttributedString(
-            string: "Send",
+            string: " Send",
             attributes: [
                 .font: NSFont.systemFont(ofSize: 14, weight: .semibold),
                 .foregroundColor: Theme.proCTAText,
                 .kern: 0.35
             ]
         )
+        findJobsButton.contentTintColor = Theme.proCTAText
         findJobsButton.isBordered = false
         findJobsButton.bezelStyle = .rounded
         findJobsButton.wantsLayer = true
@@ -1324,6 +1562,26 @@ final class DashboardView: NSView, NSTextFieldDelegate {
         window?.makeFirstResponder(nil)
     }
 
+    @objc private func didTapFeatureRole() {
+        focusSearchField(seed: "Find roles similar to: ")
+    }
+
+    @objc private func didTapFeatureCompany() {
+        focusSearchField(seed: "Find jobs at company: ")
+    }
+
+    @objc private func didTapFeatureSkill() {
+        focusSearchField(seed: "Find jobs that require skill: ")
+    }
+
+    private func focusSearchField(seed: String) {
+        jobKeywordsField.stringValue = seed
+        window?.makeFirstResponder(jobKeywordsField)
+        if let editor = jobKeywordsField.window?.fieldEditor(true, for: jobKeywordsField) as? NSTextView {
+            editor.moveToEndOfDocument(nil)
+        }
+    }
+
     @objc private func didTapLoadMoreJobs() {
         let prompt = "Show more jobs"
         guard !isAwaitingResponse, isContinuationPrompt(prompt) else { return }
@@ -1364,7 +1622,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
                     )
                     self.chatMessages.append(ChatMessage(role: "assistant", content: reply))
                     self.appendChatBubble(text: reply, isUser: false, jobs: freshJobs)
-                    self.setChatStatusLabel("Ask for another role, company, or skill match")
+                    self.setChatStatusLabel(reply)
                 case .failure(let error):
                     self.appendChatBubble(text: error.localizedDescription, isUser: false)
                     if error is URLError {
@@ -2289,6 +2547,132 @@ private struct OpenAIAPIErrorResponse: Codable {
     }
 }
 
+/// Home welcome row: three tappable shortcuts that seed the main search field (matches the reference dashboard tiles).
+private final class FeatureShortcutCardView: NSView {
+    private weak var actionTarget: AnyObject?
+    private var actionSelector: Selector
+
+    init(symbolName: String, title: String, subtitle: String, target: AnyObject?, action: Selector) {
+        self.actionTarget = target
+        self.actionSelector = action
+        super.init(frame: .zero)
+        translatesAutoresizingMaskIntoConstraints = false
+        wantsLayer = true
+        layer?.cornerRadius = 14
+        if #available(macOS 11.0, *) {
+            layer?.cornerCurve = .continuous
+        }
+        layer?.backgroundColor = NSColor.white.cgColor
+        layer?.masksToBounds = false
+        layer?.shadowColor = NSColor.black.withAlphaComponent(0.12).cgColor
+        layer?.shadowOffset = CGSize(width: 0, height: 2)
+        layer?.shadowRadius = 10
+        layer?.shadowOpacity = 1
+
+        let brandBlue = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 1)
+        let iconWellColor = NSColor(srgbRed: 220 / 255, green: 235 / 255, blue: 252 / 255, alpha: 1)
+        let secondary = NSColor(srgbRed: 118 / 255, green: 118 / 255, blue: 118 / 255, alpha: 1)
+
+        let iconHost = NSView()
+        iconHost.translatesAutoresizingMaskIntoConstraints = false
+        iconHost.wantsLayer = true
+        iconHost.layer?.backgroundColor = iconWellColor.cgColor
+        iconHost.layer?.cornerRadius = 22
+
+        let icon = NSImageView()
+        icon.translatesAutoresizingMaskIntoConstraints = false
+        icon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 16, weight: .semibold)
+        icon.image = NSImage(systemSymbolName: symbolName, accessibilityDescription: nil)
+        icon.contentTintColor = brandBlue
+        iconHost.addSubview(icon)
+
+        let titleField = NSTextField(wrappingLabelWithString: title)
+        titleField.font = .systemFont(ofSize: 15, weight: .bold)
+        titleField.textColor = brandBlue
+        titleField.maximumNumberOfLines = 1
+        titleField.isEditable = false
+        titleField.isBordered = false
+        titleField.drawsBackground = false
+        titleField.alignment = .left
+
+        let subtitleField = NSTextField(wrappingLabelWithString: subtitle)
+        subtitleField.font = .systemFont(ofSize: 11, weight: .regular)
+        subtitleField.textColor = secondary
+        subtitleField.maximumNumberOfLines = 2
+        subtitleField.isEditable = false
+        subtitleField.isBordered = false
+        subtitleField.drawsBackground = false
+        subtitleField.alignment = .left
+        subtitleField.preferredMaxLayoutWidth = 160
+
+        let textColumn = NSStackView(views: [titleField, subtitleField])
+        textColumn.orientation = .vertical
+        textColumn.spacing = 4
+        textColumn.alignment = .leading
+        textColumn.translatesAutoresizingMaskIntoConstraints = false
+        textColumn.setContentHuggingPriority(.defaultLow, for: .horizontal)
+        textColumn.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
+
+        let chevron = NSImageView()
+        chevron.translatesAutoresizingMaskIntoConstraints = false
+        chevron.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 13, weight: .semibold)
+        chevron.image = NSImage(systemSymbolName: "arrow.right", accessibilityDescription: nil)
+        chevron.contentTintColor = brandBlue
+        chevron.setContentHuggingPriority(.required, for: .horizontal)
+        chevron.setContentCompressionResistancePriority(.required, for: .horizontal)
+
+        iconHost.setContentHuggingPriority(.required, for: .horizontal)
+        iconHost.setContentCompressionResistancePriority(.required, for: .horizontal)
+
+        let row = NSStackView(views: [iconHost, textColumn, chevron])
+        row.orientation = .horizontal
+        row.spacing = 12
+        row.alignment = .centerY
+        row.distribution = .fill
+        row.translatesAutoresizingMaskIntoConstraints = false
+        addSubview(row)
+
+        NSLayoutConstraint.activate([
+            row.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 14),
+            row.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -14),
+            row.topAnchor.constraint(equalTo: topAnchor, constant: 16),
+            row.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -16),
+
+            iconHost.widthAnchor.constraint(equalToConstant: 44),
+            iconHost.heightAnchor.constraint(equalToConstant: 44),
+            icon.centerXAnchor.constraint(equalTo: iconHost.centerXAnchor),
+            icon.centerYAnchor.constraint(equalTo: iconHost.centerYAnchor)
+        ])
+
+        setAccessibilityElement(true)
+        setAccessibilityRole(.button)
+        setAccessibilityLabel("\(title). \(subtitle)")
+    }
+
+    @available(*, unavailable)
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    override func layout() {
+        super.layout()
+        guard let layer = layer, bounds.width > 0, bounds.height > 0 else { return }
+        let r = bounds
+        layer.shadowPath = CGPath(roundedRect: r, cornerWidth: 14, cornerHeight: 14, transform: nil)
+    }
+
+    override func mouseDown(with event: NSEvent) {
+        if let target = actionTarget {
+            _ = target.perform(actionSelector, with: nil)
+        }
+    }
+
+    override func resetCursorRects() {
+        super.resetCursorRects()
+        addCursorRect(bounds, cursor: .pointingHand)
+    }
+}
+
 /// `NSButton` that carries a `JobListing` for card actions (`representedObject` is unavailable on `NSButton` in this target).
 private final class JobPayloadButton: HoverableButton {
     var jobPayload: JobListing?