Selaa lähdekoodia

Refine dashboard welcome UI and Indeed-style job search bar

Apply brand blue and blue-gray to the welcome title and subtitle. Replace the single search field with a pill-shaped bar: job keywords and location columns separated by a divider, subtle shadow, charcoal border, and a Find jobs button. Wire both fields and the button to the existing search action hook.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 2 kuukautta sitten
vanhempi
sitoutus
22efaf65ae
1 muutettua tiedostoa jossa 163 lisäystä ja 47 poistoa
  1. 163 47
      App for Indeed/Views/DashboardView.swift

+ 163 - 47
App for Indeed/Views/DashboardView.swift

@@ -5,7 +5,7 @@
 
 import Cocoa
 
-final class DashboardView: NSView {
+final class DashboardView: NSView, NSTextFieldDelegate {
     /// Indeed.com-inspired neutrals and brand blue (white surfaces, `#2557a7` accent, `#2d2d2d` / `#767676` text, `#d4d2d0` borders).
     private enum Theme {
         static let brandBlue = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 1)
@@ -13,6 +13,8 @@ final class DashboardView: NSView {
         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)
         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)
@@ -20,6 +22,8 @@ final class DashboardView: NSView {
         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)
         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)
@@ -35,9 +39,14 @@ final class DashboardView: NSView {
     private let mainOverlay = NSStackView()
     private let greetingLabel = NSTextField(labelWithString: "")
     private let subtitleLabel = NSTextField(labelWithString: "")
+    private let searchBarShadowHost = NSView()
     private let searchCard = NSView()
-    private let searchIcon = NSImageView()
-    private let searchField = NSTextField()
+    private let jobSearchIcon = NSImageView()
+    private let jobKeywordsField = NSTextField()
+    private let searchDivider = NSView()
+    private let locationIcon = NSImageView()
+    private let locationField = NSTextField()
+    private let findJobsButton = NSButton()
     private let scrollView = NSScrollView()
 
     private var currentSidebarItems: [SidebarItem] = []
@@ -56,6 +65,7 @@ final class DashboardView: NSView {
     override func layout() {
         super.layout()
         updateDocumentLayout()
+        updateSearchBarShadowPath()
     }
 
     func render(_ data: DashboardData) {
@@ -124,12 +134,12 @@ final class DashboardView: NSView {
         mainOverlay.setContentHuggingPriority(.defaultLow, for: .vertical)
 
         greetingLabel.font = .systemFont(ofSize: 32, weight: .bold)
-        greetingLabel.textColor = Theme.primaryText
+        greetingLabel.textColor = Theme.brandBlue
         greetingLabel.alignment = .center
         greetingLabel.maximumNumberOfLines = 1
 
         subtitleLabel.font = .systemFont(ofSize: 15, weight: .regular)
-        subtitleLabel.textColor = Theme.secondaryText
+        subtitleLabel.textColor = Theme.welcomeSubtitleText
         subtitleLabel.alignment = .center
         subtitleLabel.maximumNumberOfLines = 2
 
@@ -137,7 +147,7 @@ final class DashboardView: NSView {
         topInset.translatesAutoresizingMaskIntoConstraints = false
         topInset.heightAnchor.constraint(equalToConstant: 32).isActive = true
 
-        configureSearchCard()
+        configureSearchBar()
 
         let titleBlock = NSStackView(views: [greetingLabel, subtitleLabel])
         titleBlock.orientation = .vertical
@@ -156,7 +166,7 @@ final class DashboardView: NSView {
         mainOverlay.addArrangedSubview(topInset)
         mainOverlay.addArrangedSubview(titleBlock)
         mainOverlay.addArrangedSubview(midSpacer)
-        mainOverlay.addArrangedSubview(searchCard)
+        mainOverlay.addArrangedSubview(searchBarShadowHost)
         mainOverlay.addArrangedSubview(overlayBottomSpacer)
 
         contentStack.addArrangedSubview(sidebar)
@@ -186,7 +196,7 @@ final class DashboardView: NSView {
             mainOverlay.topAnchor.constraint(equalTo: mainHost.topAnchor),
             mainOverlay.bottomAnchor.constraint(equalTo: mainHost.bottomAnchor, constant: -24),
 
-            searchCard.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.82),
+            searchBarShadowHost.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
 
             greetingLabel.leadingAnchor.constraint(equalTo: mainOverlay.leadingAnchor, constant: 24),
             greetingLabel.trailingAnchor.constraint(equalTo: mainOverlay.trailingAnchor, constant: -24),
@@ -195,60 +205,166 @@ final class DashboardView: NSView {
         ])
     }
 
-    private func configureSearchCard() {
+    private func configureSearchBar() {
+        let pillCorner: CGFloat = 27
+        let barHeight: CGFloat = 54
+
+        searchBarShadowHost.translatesAutoresizingMaskIntoConstraints = false
+        searchBarShadowHost.wantsLayer = true
+        searchBarShadowHost.layer?.masksToBounds = false
+        searchBarShadowHost.layer?.shadowColor = NSColor.black.withAlphaComponent(0.18).cgColor
+        searchBarShadowHost.layer?.shadowOffset = CGSize(width: 0, height: 2)
+        searchBarShadowHost.layer?.shadowRadius = 10
+        searchBarShadowHost.layer?.shadowOpacity = 1
+        searchBarShadowHost.setContentHuggingPriority(.defaultHigh, for: .vertical)
+
+        searchCard.translatesAutoresizingMaskIntoConstraints = false
         searchCard.wantsLayer = true
         searchCard.layer?.backgroundColor = Theme.cardBackground.cgColor
-        searchCard.layer?.cornerRadius = 14
+        searchCard.layer?.cornerRadius = pillCorner
         searchCard.layer?.borderWidth = 1
-        searchCard.layer?.borderColor = Theme.border.cgColor
-        searchCard.translatesAutoresizingMaskIntoConstraints = false
-        searchCard.setContentHuggingPriority(.defaultHigh, for: .vertical)
-
-        searchIcon.translatesAutoresizingMaskIntoConstraints = false
-        searchIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 15, weight: .medium)
-        searchIcon.image = NSImage(systemSymbolName: "magnifyingglass", accessibilityDescription: "Search")
-        searchIcon.contentTintColor = Theme.secondaryText
-
-        searchField.translatesAutoresizingMaskIntoConstraints = false
-        searchField.isBordered = false
-        searchField.drawsBackground = false
-        searchField.focusRingType = .none
-        searchField.font = .systemFont(ofSize: 14, weight: .regular)
-        searchField.textColor = Theme.primaryText
-        searchField.placeholderAttributedString = NSAttributedString(
-            string: "Search jobs, companies, or locations",
-            attributes: [
-                .foregroundColor: Theme.tertiaryText,
-                .font: NSFont.systemFont(ofSize: 14, weight: .regular)
-            ]
-        )
-        searchField.cell?.usesSingleLineMode = true
-        searchField.cell?.wraps = false
-        searchField.cell?.isScrollable = true
-        searchField.target = self
-        searchField.action = #selector(didSubmitSearch)
+        searchCard.layer?.borderColor = Theme.searchBarBorder.cgColor
+        searchCard.layer?.masksToBounds = true
+
+        searchBarShadowHost.addSubview(searchCard)
+
+        func configureField(_ field: NSTextField, placeholder: String) {
+            field.translatesAutoresizingMaskIntoConstraints = false
+            field.isBordered = false
+            field.drawsBackground = false
+            field.focusRingType = .none
+            field.font = .systemFont(ofSize: 14, weight: .regular)
+            field.textColor = Theme.primaryText
+            field.delegate = self
+            field.placeholderAttributedString = NSAttributedString(
+                string: placeholder,
+                attributes: [
+                    .foregroundColor: Theme.secondaryText,
+                    .font: NSFont.systemFont(ofSize: 14, weight: .regular)
+                ]
+            )
+            field.cell?.usesSingleLineMode = true
+            field.cell?.wraps = false
+            field.cell?.isScrollable = true
+            field.target = self
+            field.action = #selector(didSubmitSearch)
+        }
 
-        searchCard.addSubview(searchIcon)
-        searchCard.addSubview(searchField)
+        jobSearchIcon.translatesAutoresizingMaskIntoConstraints = false
+        jobSearchIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 15, weight: .medium)
+        jobSearchIcon.image = NSImage(systemSymbolName: "magnifyingglass", accessibilityDescription: "Job search")
+        jobSearchIcon.contentTintColor = Theme.primaryText
+
+        configureField(jobKeywordsField, placeholder: "Job title, keywords, or company")
+
+        searchDivider.translatesAutoresizingMaskIntoConstraints = false
+        searchDivider.wantsLayer = true
+        searchDivider.layer?.backgroundColor = Theme.border.cgColor
+
+        locationIcon.translatesAutoresizingMaskIntoConstraints = false
+        locationIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 15, weight: .medium)
+        locationIcon.image = NSImage(systemSymbolName: "mappin.and.ellipse", accessibilityDescription: "Location")
+        locationIcon.contentTintColor = Theme.primaryText
+
+        configureField(locationField, placeholder: "City, state, zip code, or \"remote\"")
+
+        findJobsButton.translatesAutoresizingMaskIntoConstraints = false
+        findJobsButton.title = "Find jobs"
+        findJobsButton.isBordered = false
+        findJobsButton.bezelStyle = .rounded
+        findJobsButton.font = .systemFont(ofSize: 14, weight: .bold)
+        findJobsButton.contentTintColor = Theme.proCTAText
+        findJobsButton.wantsLayer = true
+        findJobsButton.layer?.backgroundColor = Theme.brandBlue.cgColor
+        findJobsButton.layer?.cornerRadius = 10
+        findJobsButton.target = self
+        findJobsButton.action = #selector(didSubmitSearch)
+        findJobsButton.setContentHuggingPriority(.required, for: .horizontal)
+        findJobsButton.setContentCompressionResistancePriority(.required, for: .horizontal)
+
+        let keywordsStack = NSStackView(views: [jobSearchIcon, jobKeywordsField])
+        keywordsStack.orientation = .horizontal
+        keywordsStack.spacing = 10
+        keywordsStack.alignment = .centerY
+        keywordsStack.translatesAutoresizingMaskIntoConstraints = false
+        keywordsStack.edgeInsets = NSEdgeInsets(top: 0, left: 18, bottom: 0, right: 10)
+
+        let locationStack = NSStackView(views: [locationIcon, locationField])
+        locationStack.orientation = .horizontal
+        locationStack.spacing = 10
+        locationStack.alignment = .centerY
+        locationStack.translatesAutoresizingMaskIntoConstraints = false
+        locationStack.edgeInsets = NSEdgeInsets(top: 0, left: 10, bottom: 0, right: 10)
+
+        let row = NSStackView(views: [keywordsStack, searchDivider, locationStack, findJobsButton])
+        row.orientation = .horizontal
+        row.spacing = 0
+        row.alignment = .centerY
+        row.distribution = .fill
+        row.translatesAutoresizingMaskIntoConstraints = false
+        row.edgeInsets = NSEdgeInsets(top: 0, left: 0, bottom: 0, right: 10)
+
+        searchCard.addSubview(row)
 
         NSLayoutConstraint.activate([
-            searchCard.heightAnchor.constraint(equalToConstant: 48),
+            searchCard.leadingAnchor.constraint(equalTo: searchBarShadowHost.leadingAnchor),
+            searchCard.trailingAnchor.constraint(equalTo: searchBarShadowHost.trailingAnchor),
+            searchCard.topAnchor.constraint(equalTo: searchBarShadowHost.topAnchor),
+            searchCard.bottomAnchor.constraint(equalTo: searchBarShadowHost.bottomAnchor),
+
+            searchBarShadowHost.heightAnchor.constraint(equalToConstant: barHeight),
+
+            row.leadingAnchor.constraint(equalTo: searchCard.leadingAnchor),
+            row.trailingAnchor.constraint(equalTo: searchCard.trailingAnchor),
+            row.topAnchor.constraint(equalTo: searchCard.topAnchor),
+            row.bottomAnchor.constraint(equalTo: searchCard.bottomAnchor),
+
+            jobSearchIcon.widthAnchor.constraint(equalToConstant: 18),
+            jobSearchIcon.heightAnchor.constraint(equalToConstant: 18),
+            locationIcon.widthAnchor.constraint(equalToConstant: 18),
+            locationIcon.heightAnchor.constraint(equalToConstant: 18),
+
+            searchDivider.widthAnchor.constraint(equalToConstant: 1),
+            searchDivider.heightAnchor.constraint(equalToConstant: 30),
 
-            searchIcon.leadingAnchor.constraint(equalTo: searchCard.leadingAnchor, constant: 16),
-            searchIcon.centerYAnchor.constraint(equalTo: searchCard.centerYAnchor),
-            searchIcon.widthAnchor.constraint(equalToConstant: 18),
-            searchIcon.heightAnchor.constraint(equalToConstant: 18),
+            keywordsStack.widthAnchor.constraint(equalTo: locationStack.widthAnchor),
 
-            searchField.leadingAnchor.constraint(equalTo: searchIcon.trailingAnchor, constant: 10),
-            searchField.trailingAnchor.constraint(equalTo: searchCard.trailingAnchor, constant: -16),
-            searchField.centerYAnchor.constraint(equalTo: searchCard.centerYAnchor)
+            findJobsButton.heightAnchor.constraint(equalToConstant: 40),
+            findJobsButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 108)
         ])
     }
 
+    private func updateSearchBarShadowPath() {
+        guard searchBarShadowHost.bounds.width > 0, searchBarShadowHost.bounds.height > 0 else { return }
+        let r = searchBarShadowHost.bounds
+        let radius = min(r.height / 2, 27)
+        searchBarShadowHost.layer?.shadowPath = CGPath(
+            roundedRect: r,
+            cornerWidth: radius,
+            cornerHeight: radius,
+            transform: nil
+        )
+    }
+
     @objc private func didSubmitSearch() {
         // Hook up search submission here when wiring up real data.
     }
 
+    func controlTextDidBeginEditing(_ obj: Notification) {
+        applySearchFieldInsertionPoint(obj.object)
+    }
+
+    func controlTextDidChange(_ obj: Notification) {
+        applySearchFieldInsertionPoint(obj.object)
+    }
+
+    private func applySearchFieldInsertionPoint(_ object: Any?) {
+        guard let field = object as? NSTextField,
+              field === jobKeywordsField || field === locationField,
+              let textView = field.window?.fieldEditor(true, for: field) as? NSTextView else { return }
+        textView.insertionPointColor = Theme.primaryText
+    }
+
     private func configureSidebar() {
         let items = currentSidebarItems
         sidebar.arrangedSubviews.forEach {