|
|
@@ -4,6 +4,7 @@
|
|
4
|
4
|
//
|
|
5
|
5
|
|
|
6
|
6
|
import Cocoa
|
|
|
7
|
+import QuartzCore
|
|
7
|
8
|
|
|
8
|
9
|
final class DashboardView: NSView, NSTextFieldDelegate {
|
|
9
|
10
|
/// Indeed.com-inspired neutrals and brand blue (white surfaces, `#2557a7` accent, `#2d2d2d` / `#767676` text, `#d4d2d0` borders).
|
|
|
@@ -29,6 +30,8 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
29
|
30
|
static let proAccent = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 1)
|
|
30
|
31
|
static let proCTABackground = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 1)
|
|
31
|
32
|
static let proCTAText = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
|
|
|
33
|
+ /// Slightly lighter blue for the top of the search-bar “Find jobs” pill (reads less flat than a solid fill).
|
|
|
34
|
+ static let findJobsCTAHighlight = NSColor(srgbRed: 54 / 255, green: 110 / 255, blue: 198 / 255, alpha: 1)
|
|
32
|
35
|
}
|
|
33
|
36
|
|
|
34
|
37
|
private let contentStack = NSStackView()
|
|
|
@@ -43,10 +46,10 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
43
|
46
|
private let searchCard = NSView()
|
|
44
|
47
|
private let jobSearchIcon = NSImageView()
|
|
45
|
48
|
private let jobKeywordsField = NSTextField()
|
|
46
|
|
- private let searchDivider = NSView()
|
|
47
|
|
- private let locationIcon = NSImageView()
|
|
48
|
|
- private let locationField = NSTextField()
|
|
49
|
49
|
private let findJobsButton = NSButton()
|
|
|
50
|
+ private let findJobsCTAHost = NSView()
|
|
|
51
|
+ private let findJobsCTAChrome = NSView()
|
|
|
52
|
+ private var findJobsCTAGradientLayer: CAGradientLayer?
|
|
50
|
53
|
private let scrollView = NSScrollView()
|
|
51
|
54
|
|
|
52
|
55
|
private var currentSidebarItems: [SidebarItem] = []
|
|
|
@@ -66,6 +69,8 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
66
|
69
|
super.layout()
|
|
67
|
70
|
updateDocumentLayout()
|
|
68
|
71
|
updateSearchBarShadowPath()
|
|
|
72
|
+ findJobsCTAGradientLayer?.frame = findJobsCTAChrome.bounds
|
|
|
73
|
+ updateFindJobsCTAShadowPath()
|
|
69
|
74
|
}
|
|
70
|
75
|
|
|
71
|
76
|
func render(_ data: DashboardData) {
|
|
|
@@ -257,52 +262,81 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
257
|
262
|
|
|
258
|
263
|
configureField(jobKeywordsField, placeholder: "Job title, keywords, or company")
|
|
259
|
264
|
|
|
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
|
|
|
265
|
+ let ctaHeight: CGFloat = 42
|
|
|
266
|
+ let ctaCorner = ctaHeight / 2
|
|
|
267
|
+
|
|
|
268
|
+ findJobsCTAHost.translatesAutoresizingMaskIntoConstraints = false
|
|
|
269
|
+ findJobsCTAHost.wantsLayer = true
|
|
|
270
|
+ findJobsCTAHost.layer?.masksToBounds = false
|
|
|
271
|
+ findJobsCTAHost.layer?.shadowColor = NSColor.black.cgColor
|
|
|
272
|
+ findJobsCTAHost.layer?.shadowOpacity = 0.16
|
|
|
273
|
+ findJobsCTAHost.layer?.shadowOffset = CGSize(width: 0, height: 2)
|
|
|
274
|
+ findJobsCTAHost.layer?.shadowRadius = 6
|
|
|
275
|
+
|
|
|
276
|
+ findJobsCTAChrome.translatesAutoresizingMaskIntoConstraints = false
|
|
|
277
|
+ findJobsCTAChrome.wantsLayer = true
|
|
|
278
|
+ findJobsCTAChrome.layer?.masksToBounds = true
|
|
|
279
|
+ findJobsCTAChrome.layer?.cornerRadius = ctaCorner
|
|
|
280
|
+ if #available(macOS 11.0, *) {
|
|
|
281
|
+ findJobsCTAChrome.layer?.cornerCurve = .continuous
|
|
|
282
|
+ }
|
|
268
|
283
|
|
|
269
|
|
- configureField(locationField, placeholder: "City, state, zip code, or \"remote\"")
|
|
|
284
|
+ let gradient = CAGradientLayer()
|
|
|
285
|
+ gradient.colors = [Theme.findJobsCTAHighlight.cgColor, Theme.brandBlue.cgColor]
|
|
|
286
|
+ gradient.startPoint = CGPoint(x: 0.5, y: 1)
|
|
|
287
|
+ gradient.endPoint = CGPoint(x: 0.5, y: 0)
|
|
|
288
|
+ findJobsCTAChrome.layer?.addSublayer(gradient)
|
|
|
289
|
+ findJobsCTAGradientLayer = gradient
|
|
270
|
290
|
|
|
271
|
291
|
findJobsButton.translatesAutoresizingMaskIntoConstraints = false
|
|
272
|
|
- findJobsButton.title = "Find jobs"
|
|
|
292
|
+ findJobsButton.title = ""
|
|
|
293
|
+ findJobsButton.attributedTitle = NSAttributedString(
|
|
|
294
|
+ string: "Find jobs",
|
|
|
295
|
+ attributes: [
|
|
|
296
|
+ .font: NSFont.systemFont(ofSize: 14, weight: .semibold),
|
|
|
297
|
+ .foregroundColor: Theme.proCTAText,
|
|
|
298
|
+ .kern: 0.35
|
|
|
299
|
+ ]
|
|
|
300
|
+ )
|
|
273
|
301
|
findJobsButton.isBordered = false
|
|
274
|
302
|
findJobsButton.bezelStyle = .rounded
|
|
275
|
|
- findJobsButton.font = .systemFont(ofSize: 14, weight: .bold)
|
|
276
|
|
- findJobsButton.contentTintColor = Theme.proCTAText
|
|
277
|
303
|
findJobsButton.wantsLayer = true
|
|
278
|
|
- findJobsButton.layer?.backgroundColor = Theme.brandBlue.cgColor
|
|
279
|
|
- findJobsButton.layer?.cornerRadius = 10
|
|
|
304
|
+ findJobsButton.layer?.backgroundColor = NSColor.clear.cgColor
|
|
|
305
|
+ findJobsButton.focusRingType = .none
|
|
280
|
306
|
findJobsButton.target = self
|
|
281
|
307
|
findJobsButton.action = #selector(didSubmitSearch)
|
|
282
|
308
|
findJobsButton.setContentHuggingPriority(.required, for: .horizontal)
|
|
283
|
309
|
findJobsButton.setContentCompressionResistancePriority(.required, for: .horizontal)
|
|
284
|
310
|
|
|
|
311
|
+ findJobsCTAHost.addSubview(findJobsCTAChrome)
|
|
|
312
|
+ findJobsCTAHost.addSubview(findJobsButton)
|
|
|
313
|
+ NSLayoutConstraint.activate([
|
|
|
314
|
+ findJobsCTAChrome.leadingAnchor.constraint(equalTo: findJobsCTAHost.leadingAnchor),
|
|
|
315
|
+ findJobsCTAChrome.trailingAnchor.constraint(equalTo: findJobsCTAHost.trailingAnchor),
|
|
|
316
|
+ findJobsCTAChrome.topAnchor.constraint(equalTo: findJobsCTAHost.topAnchor),
|
|
|
317
|
+ findJobsCTAChrome.bottomAnchor.constraint(equalTo: findJobsCTAHost.bottomAnchor),
|
|
|
318
|
+
|
|
|
319
|
+ findJobsButton.leadingAnchor.constraint(equalTo: findJobsCTAHost.leadingAnchor, constant: 14),
|
|
|
320
|
+ findJobsButton.trailingAnchor.constraint(equalTo: findJobsCTAHost.trailingAnchor, constant: -14),
|
|
|
321
|
+ findJobsButton.topAnchor.constraint(equalTo: findJobsCTAHost.topAnchor),
|
|
|
322
|
+ findJobsButton.bottomAnchor.constraint(equalTo: findJobsCTAHost.bottomAnchor)
|
|
|
323
|
+ ])
|
|
|
324
|
+
|
|
285
|
325
|
let keywordsStack = NSStackView(views: [jobSearchIcon, jobKeywordsField])
|
|
286
|
326
|
keywordsStack.orientation = .horizontal
|
|
287
|
327
|
keywordsStack.spacing = 10
|
|
288
|
328
|
keywordsStack.alignment = .centerY
|
|
289
|
329
|
keywordsStack.translatesAutoresizingMaskIntoConstraints = false
|
|
290
|
330
|
keywordsStack.edgeInsets = NSEdgeInsets(top: 0, left: 18, bottom: 0, right: 10)
|
|
|
331
|
+ keywordsStack.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
291
|
332
|
|
|
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])
|
|
|
333
|
+ let row = NSStackView(views: [keywordsStack, findJobsCTAHost])
|
|
300
|
334
|
row.orientation = .horizontal
|
|
301
|
335
|
row.spacing = 0
|
|
302
|
336
|
row.alignment = .centerY
|
|
303
|
337
|
row.distribution = .fill
|
|
304
|
338
|
row.translatesAutoresizingMaskIntoConstraints = false
|
|
305
|
|
- row.edgeInsets = NSEdgeInsets(top: 0, left: 0, bottom: 0, right: 10)
|
|
|
339
|
+ row.edgeInsets = NSEdgeInsets(top: 0, left: 0, bottom: 0, right: 7)
|
|
306
|
340
|
|
|
307
|
341
|
searchCard.addSubview(row)
|
|
308
|
342
|
|
|
|
@@ -321,19 +355,24 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
321
|
355
|
|
|
322
|
356
|
jobSearchIcon.widthAnchor.constraint(equalToConstant: 18),
|
|
323
|
357
|
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),
|
|
329
|
358
|
|
|
330
|
|
- keywordsStack.widthAnchor.constraint(equalTo: locationStack.widthAnchor),
|
|
331
|
|
-
|
|
332
|
|
- findJobsButton.heightAnchor.constraint(equalToConstant: 40),
|
|
333
|
|
- findJobsButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 108)
|
|
|
359
|
+ findJobsCTAHost.heightAnchor.constraint(equalToConstant: ctaHeight),
|
|
|
360
|
+ findJobsCTAHost.widthAnchor.constraint(greaterThanOrEqualToConstant: 112)
|
|
334
|
361
|
])
|
|
335
|
362
|
}
|
|
336
|
363
|
|
|
|
364
|
+ private func updateFindJobsCTAShadowPath() {
|
|
|
365
|
+ guard findJobsCTAHost.bounds.width > 0, findJobsCTAHost.bounds.height > 0 else { return }
|
|
|
366
|
+ let r = findJobsCTAHost.bounds
|
|
|
367
|
+ let radius = min(r.height / 2, r.width / 2)
|
|
|
368
|
+ findJobsCTAHost.layer?.shadowPath = CGPath(
|
|
|
369
|
+ roundedRect: r,
|
|
|
370
|
+ cornerWidth: radius,
|
|
|
371
|
+ cornerHeight: radius,
|
|
|
372
|
+ transform: nil
|
|
|
373
|
+ )
|
|
|
374
|
+ }
|
|
|
375
|
+
|
|
337
|
376
|
private func updateSearchBarShadowPath() {
|
|
338
|
377
|
guard searchBarShadowHost.bounds.width > 0, searchBarShadowHost.bounds.height > 0 else { return }
|
|
339
|
378
|
let r = searchBarShadowHost.bounds
|
|
|
@@ -360,7 +399,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
360
|
399
|
|
|
361
|
400
|
private func applySearchFieldInsertionPoint(_ object: Any?) {
|
|
362
|
401
|
guard let field = object as? NSTextField,
|
|
363
|
|
- field === jobKeywordsField || field === locationField,
|
|
|
402
|
+ field === jobKeywordsField,
|
|
364
|
403
|
let textView = field.window?.fieldEditor(true, for: field) as? NSTextView else { return }
|
|
365
|
404
|
textView.insertionPointColor = Theme.primaryText
|
|
366
|
405
|
}
|