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

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 недель назад: 3
Родитель
Сommit
22efaf65ae
1 измененных файлов с 163 добавлено и 47 удалено
  1. 163 47
      App for Indeed/Views/DashboardView.swift

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

@@ -5,7 +5,7 @@
5 5
 
6 6
 import Cocoa
7 7
 
8
-final class DashboardView: NSView {
8
+final class DashboardView: NSView, NSTextFieldDelegate {
9 9
     /// Indeed.com-inspired neutrals and brand blue (white surfaces, `#2557a7` accent, `#2d2d2d` / `#767676` text, `#d4d2d0` borders).
10 10
     private enum Theme {
11 11
         static let brandBlue = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 1)
@@ -13,6 +13,8 @@ final class DashboardView: NSView {
13 13
         static let chromeBackground = NSColor(srgbRed: 247 / 255, green: 247 / 255, blue: 247 / 255, alpha: 1)
14 14
         static let sidebarBackground = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
15 15
         static let mainHostBackground = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
16
+        /// Subtitle on the welcome hero: readable blue-gray aligned with brand.
17
+        static let welcomeSubtitleText = NSColor(srgbRed: 52 / 255, green: 92 / 255, blue: 142 / 255, alpha: 1)
16 18
         static let selectionFill = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 0.12)
17 19
         static let cardBackground = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
18 20
         static let toggleBackground = NSColor(srgbRed: 232 / 255, green: 232 / 255, blue: 232 / 255, alpha: 1)
@@ -20,6 +22,8 @@ final class DashboardView: NSView {
20 22
         static let secondaryText = NSColor(srgbRed: 118 / 255, green: 118 / 255, blue: 118 / 255, alpha: 1)
21 23
         static let tertiaryText = NSColor(srgbRed: 118 / 255, green: 118 / 255, blue: 118 / 255, alpha: 1)
22 24
         static let border = NSColor(srgbRed: 212 / 255, green: 210 / 255, blue: 208 / 255, alpha: 1)
25
+        /// Job search bar outer stroke (charcoal).
26
+        static let searchBarBorder = NSColor(srgbRed: 58 / 255, green: 58 / 255, blue: 58 / 255, alpha: 1)
23 27
         static let proCardFill = NSColor(srgbRed: 239 / 255, green: 244 / 255, blue: 252 / 255, alpha: 1)
24 28
         static let proCardBorder = NSColor(srgbRed: 212 / 255, green: 210 / 255, blue: 208 / 255, alpha: 1)
25 29
         static let proAccent = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 1)
@@ -35,9 +39,14 @@ final class DashboardView: NSView {
35 39
     private let mainOverlay = NSStackView()
36 40
     private let greetingLabel = NSTextField(labelWithString: "")
37 41
     private let subtitleLabel = NSTextField(labelWithString: "")
42
+    private let searchBarShadowHost = NSView()
38 43
     private let searchCard = NSView()
39
-    private let searchIcon = NSImageView()
40
-    private let searchField = NSTextField()
44
+    private let jobSearchIcon = NSImageView()
45
+    private let jobKeywordsField = NSTextField()
46
+    private let searchDivider = NSView()
47
+    private let locationIcon = NSImageView()
48
+    private let locationField = NSTextField()
49
+    private let findJobsButton = NSButton()
41 50
     private let scrollView = NSScrollView()
42 51
 
43 52
     private var currentSidebarItems: [SidebarItem] = []
@@ -56,6 +65,7 @@ final class DashboardView: NSView {
56 65
     override func layout() {
57 66
         super.layout()
58 67
         updateDocumentLayout()
68
+        updateSearchBarShadowPath()
59 69
     }
60 70
 
61 71
     func render(_ data: DashboardData) {
@@ -124,12 +134,12 @@ final class DashboardView: NSView {
124 134
         mainOverlay.setContentHuggingPriority(.defaultLow, for: .vertical)
125 135
 
126 136
         greetingLabel.font = .systemFont(ofSize: 32, weight: .bold)
127
-        greetingLabel.textColor = Theme.primaryText
137
+        greetingLabel.textColor = Theme.brandBlue
128 138
         greetingLabel.alignment = .center
129 139
         greetingLabel.maximumNumberOfLines = 1
130 140
 
131 141
         subtitleLabel.font = .systemFont(ofSize: 15, weight: .regular)
132
-        subtitleLabel.textColor = Theme.secondaryText
142
+        subtitleLabel.textColor = Theme.welcomeSubtitleText
133 143
         subtitleLabel.alignment = .center
134 144
         subtitleLabel.maximumNumberOfLines = 2
135 145
 
@@ -137,7 +147,7 @@ final class DashboardView: NSView {
137 147
         topInset.translatesAutoresizingMaskIntoConstraints = false
138 148
         topInset.heightAnchor.constraint(equalToConstant: 32).isActive = true
139 149
 
140
-        configureSearchCard()
150
+        configureSearchBar()
141 151
 
142 152
         let titleBlock = NSStackView(views: [greetingLabel, subtitleLabel])
143 153
         titleBlock.orientation = .vertical
@@ -156,7 +166,7 @@ final class DashboardView: NSView {
156 166
         mainOverlay.addArrangedSubview(topInset)
157 167
         mainOverlay.addArrangedSubview(titleBlock)
158 168
         mainOverlay.addArrangedSubview(midSpacer)
159
-        mainOverlay.addArrangedSubview(searchCard)
169
+        mainOverlay.addArrangedSubview(searchBarShadowHost)
160 170
         mainOverlay.addArrangedSubview(overlayBottomSpacer)
161 171
 
162 172
         contentStack.addArrangedSubview(sidebar)
@@ -186,7 +196,7 @@ final class DashboardView: NSView {
186 196
             mainOverlay.topAnchor.constraint(equalTo: mainHost.topAnchor),
187 197
             mainOverlay.bottomAnchor.constraint(equalTo: mainHost.bottomAnchor, constant: -24),
188 198
 
189
-            searchCard.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.82),
199
+            searchBarShadowHost.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
190 200
 
191 201
             greetingLabel.leadingAnchor.constraint(equalTo: mainOverlay.leadingAnchor, constant: 24),
192 202
             greetingLabel.trailingAnchor.constraint(equalTo: mainOverlay.trailingAnchor, constant: -24),
@@ -195,60 +205,166 @@ final class DashboardView: NSView {
195 205
         ])
196 206
     }
197 207
 
198
-    private func configureSearchCard() {
208
+    private func configureSearchBar() {
209
+        let pillCorner: CGFloat = 27
210
+        let barHeight: CGFloat = 54
211
+
212
+        searchBarShadowHost.translatesAutoresizingMaskIntoConstraints = false
213
+        searchBarShadowHost.wantsLayer = true
214
+        searchBarShadowHost.layer?.masksToBounds = false
215
+        searchBarShadowHost.layer?.shadowColor = NSColor.black.withAlphaComponent(0.18).cgColor
216
+        searchBarShadowHost.layer?.shadowOffset = CGSize(width: 0, height: 2)
217
+        searchBarShadowHost.layer?.shadowRadius = 10
218
+        searchBarShadowHost.layer?.shadowOpacity = 1
219
+        searchBarShadowHost.setContentHuggingPriority(.defaultHigh, for: .vertical)
220
+
221
+        searchCard.translatesAutoresizingMaskIntoConstraints = false
199 222
         searchCard.wantsLayer = true
200 223
         searchCard.layer?.backgroundColor = Theme.cardBackground.cgColor
201
-        searchCard.layer?.cornerRadius = 14
224
+        searchCard.layer?.cornerRadius = pillCorner
202 225
         searchCard.layer?.borderWidth = 1
203
-        searchCard.layer?.borderColor = Theme.border.cgColor
204
-        searchCard.translatesAutoresizingMaskIntoConstraints = false
205
-        searchCard.setContentHuggingPriority(.defaultHigh, for: .vertical)
206
-
207
-        searchIcon.translatesAutoresizingMaskIntoConstraints = false
208
-        searchIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 15, weight: .medium)
209
-        searchIcon.image = NSImage(systemSymbolName: "magnifyingglass", accessibilityDescription: "Search")
210
-        searchIcon.contentTintColor = Theme.secondaryText
211
-
212
-        searchField.translatesAutoresizingMaskIntoConstraints = false
213
-        searchField.isBordered = false
214
-        searchField.drawsBackground = false
215
-        searchField.focusRingType = .none
216
-        searchField.font = .systemFont(ofSize: 14, weight: .regular)
217
-        searchField.textColor = Theme.primaryText
218
-        searchField.placeholderAttributedString = NSAttributedString(
219
-            string: "Search jobs, companies, or locations",
220
-            attributes: [
221
-                .foregroundColor: Theme.tertiaryText,
222
-                .font: NSFont.systemFont(ofSize: 14, weight: .regular)
223
-            ]
224
-        )
225
-        searchField.cell?.usesSingleLineMode = true
226
-        searchField.cell?.wraps = false
227
-        searchField.cell?.isScrollable = true
228
-        searchField.target = self
229
-        searchField.action = #selector(didSubmitSearch)
226
+        searchCard.layer?.borderColor = Theme.searchBarBorder.cgColor
227
+        searchCard.layer?.masksToBounds = true
228
+
229
+        searchBarShadowHost.addSubview(searchCard)
230
+
231
+        func configureField(_ field: NSTextField, placeholder: String) {
232
+            field.translatesAutoresizingMaskIntoConstraints = false
233
+            field.isBordered = false
234
+            field.drawsBackground = false
235
+            field.focusRingType = .none
236
+            field.font = .systemFont(ofSize: 14, weight: .regular)
237
+            field.textColor = Theme.primaryText
238
+            field.delegate = self
239
+            field.placeholderAttributedString = NSAttributedString(
240
+                string: placeholder,
241
+                attributes: [
242
+                    .foregroundColor: Theme.secondaryText,
243
+                    .font: NSFont.systemFont(ofSize: 14, weight: .regular)
244
+                ]
245
+            )
246
+            field.cell?.usesSingleLineMode = true
247
+            field.cell?.wraps = false
248
+            field.cell?.isScrollable = true
249
+            field.target = self
250
+            field.action = #selector(didSubmitSearch)
251
+        }
230 252
 
231
-        searchCard.addSubview(searchIcon)
232
-        searchCard.addSubview(searchField)
253
+        jobSearchIcon.translatesAutoresizingMaskIntoConstraints = false
254
+        jobSearchIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 15, weight: .medium)
255
+        jobSearchIcon.image = NSImage(systemSymbolName: "magnifyingglass", accessibilityDescription: "Job search")
256
+        jobSearchIcon.contentTintColor = Theme.primaryText
257
+
258
+        configureField(jobKeywordsField, placeholder: "Job title, keywords, or company")
259
+
260
+        searchDivider.translatesAutoresizingMaskIntoConstraints = false
261
+        searchDivider.wantsLayer = true
262
+        searchDivider.layer?.backgroundColor = Theme.border.cgColor
263
+
264
+        locationIcon.translatesAutoresizingMaskIntoConstraints = false
265
+        locationIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 15, weight: .medium)
266
+        locationIcon.image = NSImage(systemSymbolName: "mappin.and.ellipse", accessibilityDescription: "Location")
267
+        locationIcon.contentTintColor = Theme.primaryText
268
+
269
+        configureField(locationField, placeholder: "City, state, zip code, or \"remote\"")
270
+
271
+        findJobsButton.translatesAutoresizingMaskIntoConstraints = false
272
+        findJobsButton.title = "Find jobs"
273
+        findJobsButton.isBordered = false
274
+        findJobsButton.bezelStyle = .rounded
275
+        findJobsButton.font = .systemFont(ofSize: 14, weight: .bold)
276
+        findJobsButton.contentTintColor = Theme.proCTAText
277
+        findJobsButton.wantsLayer = true
278
+        findJobsButton.layer?.backgroundColor = Theme.brandBlue.cgColor
279
+        findJobsButton.layer?.cornerRadius = 10
280
+        findJobsButton.target = self
281
+        findJobsButton.action = #selector(didSubmitSearch)
282
+        findJobsButton.setContentHuggingPriority(.required, for: .horizontal)
283
+        findJobsButton.setContentCompressionResistancePriority(.required, for: .horizontal)
284
+
285
+        let keywordsStack = NSStackView(views: [jobSearchIcon, jobKeywordsField])
286
+        keywordsStack.orientation = .horizontal
287
+        keywordsStack.spacing = 10
288
+        keywordsStack.alignment = .centerY
289
+        keywordsStack.translatesAutoresizingMaskIntoConstraints = false
290
+        keywordsStack.edgeInsets = NSEdgeInsets(top: 0, left: 18, bottom: 0, right: 10)
291
+
292
+        let locationStack = NSStackView(views: [locationIcon, locationField])
293
+        locationStack.orientation = .horizontal
294
+        locationStack.spacing = 10
295
+        locationStack.alignment = .centerY
296
+        locationStack.translatesAutoresizingMaskIntoConstraints = false
297
+        locationStack.edgeInsets = NSEdgeInsets(top: 0, left: 10, bottom: 0, right: 10)
298
+
299
+        let row = NSStackView(views: [keywordsStack, searchDivider, locationStack, findJobsButton])
300
+        row.orientation = .horizontal
301
+        row.spacing = 0
302
+        row.alignment = .centerY
303
+        row.distribution = .fill
304
+        row.translatesAutoresizingMaskIntoConstraints = false
305
+        row.edgeInsets = NSEdgeInsets(top: 0, left: 0, bottom: 0, right: 10)
306
+
307
+        searchCard.addSubview(row)
233 308
 
234 309
         NSLayoutConstraint.activate([
235
-            searchCard.heightAnchor.constraint(equalToConstant: 48),
310
+            searchCard.leadingAnchor.constraint(equalTo: searchBarShadowHost.leadingAnchor),
311
+            searchCard.trailingAnchor.constraint(equalTo: searchBarShadowHost.trailingAnchor),
312
+            searchCard.topAnchor.constraint(equalTo: searchBarShadowHost.topAnchor),
313
+            searchCard.bottomAnchor.constraint(equalTo: searchBarShadowHost.bottomAnchor),
314
+
315
+            searchBarShadowHost.heightAnchor.constraint(equalToConstant: barHeight),
316
+
317
+            row.leadingAnchor.constraint(equalTo: searchCard.leadingAnchor),
318
+            row.trailingAnchor.constraint(equalTo: searchCard.trailingAnchor),
319
+            row.topAnchor.constraint(equalTo: searchCard.topAnchor),
320
+            row.bottomAnchor.constraint(equalTo: searchCard.bottomAnchor),
321
+
322
+            jobSearchIcon.widthAnchor.constraint(equalToConstant: 18),
323
+            jobSearchIcon.heightAnchor.constraint(equalToConstant: 18),
324
+            locationIcon.widthAnchor.constraint(equalToConstant: 18),
325
+            locationIcon.heightAnchor.constraint(equalToConstant: 18),
326
+
327
+            searchDivider.widthAnchor.constraint(equalToConstant: 1),
328
+            searchDivider.heightAnchor.constraint(equalToConstant: 30),
236 329
 
237
-            searchIcon.leadingAnchor.constraint(equalTo: searchCard.leadingAnchor, constant: 16),
238
-            searchIcon.centerYAnchor.constraint(equalTo: searchCard.centerYAnchor),
239
-            searchIcon.widthAnchor.constraint(equalToConstant: 18),
240
-            searchIcon.heightAnchor.constraint(equalToConstant: 18),
330
+            keywordsStack.widthAnchor.constraint(equalTo: locationStack.widthAnchor),
241 331
 
242
-            searchField.leadingAnchor.constraint(equalTo: searchIcon.trailingAnchor, constant: 10),
243
-            searchField.trailingAnchor.constraint(equalTo: searchCard.trailingAnchor, constant: -16),
244
-            searchField.centerYAnchor.constraint(equalTo: searchCard.centerYAnchor)
332
+            findJobsButton.heightAnchor.constraint(equalToConstant: 40),
333
+            findJobsButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 108)
245 334
         ])
246 335
     }
247 336
 
337
+    private func updateSearchBarShadowPath() {
338
+        guard searchBarShadowHost.bounds.width > 0, searchBarShadowHost.bounds.height > 0 else { return }
339
+        let r = searchBarShadowHost.bounds
340
+        let radius = min(r.height / 2, 27)
341
+        searchBarShadowHost.layer?.shadowPath = CGPath(
342
+            roundedRect: r,
343
+            cornerWidth: radius,
344
+            cornerHeight: radius,
345
+            transform: nil
346
+        )
347
+    }
348
+
248 349
     @objc private func didSubmitSearch() {
249 350
         // Hook up search submission here when wiring up real data.
250 351
     }
251 352
 
353
+    func controlTextDidBeginEditing(_ obj: Notification) {
354
+        applySearchFieldInsertionPoint(obj.object)
355
+    }
356
+
357
+    func controlTextDidChange(_ obj: Notification) {
358
+        applySearchFieldInsertionPoint(obj.object)
359
+    }
360
+
361
+    private func applySearchFieldInsertionPoint(_ object: Any?) {
362
+        guard let field = object as? NSTextField,
363
+              field === jobKeywordsField || field === locationField,
364
+              let textView = field.window?.fieldEditor(true, for: field) as? NSTextView else { return }
365
+        textView.insertionPointColor = Theme.primaryText
366
+    }
367
+
252 368
     private func configureSidebar() {
253 369
         let items = currentSidebarItems
254 370
         sidebar.arrangedSubviews.forEach {