Преглед изворни кода

Redesign welcome hero to match reference layout

Restack sparkle badge above Welcome title with pastel well and layered
sparkles; add drawn wave and sparkle decorations; align typography and
spacing (#0052CC / #334155). Remove subtitle breathing animation; match
feature card primary blue to hero.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 пре 3 недеља
родитељ
комит
dcb606d9ed
1 измењених фајлова са 181 додато и 55 уклоњено
  1. 181 55
      App for Indeed/Views/DashboardView.swift

+ 181 - 55
App for Indeed/Views/DashboardView.swift

@@ -20,6 +20,12 @@ 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
+        /// Welcome hero (matches reference: `#0052CC` heading, `#334155` subline, `#EFF6FF` icon well).
24
+        static let welcomeHeroHeadingBlue = NSColor(srgbRed: 0, green: 82 / 255, blue: 204 / 255, alpha: 1)
25
+        static let welcomeHeroSubtitleText = NSColor(srgbRed: 51 / 255, green: 65 / 255, blue: 85 / 255, alpha: 1)
26
+        static let welcomeHeroIconWell = NSColor(srgbRed: 239 / 255, green: 246 / 255, blue: 255 / 255, alpha: 1)
27
+        /// Light decorative strokes / sparkles behind the welcome hero.
28
+        static let welcomeHeroWaveTint = NSColor(srgbRed: 186 / 255, green: 210 / 255, blue: 253 / 255, alpha: 1)
23 29
         /// Subtitle on the welcome hero: dark neutral gray to match the reference layout.
24 30
         static let welcomeSubtitleText = NSColor(srgbRed: 64 / 255, green: 64 / 255, blue: 64 / 255, alpha: 1)
25 31
         static let selectionFill = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 0.12)
@@ -82,7 +88,11 @@ final class DashboardView: NSView, NSTextFieldDelegate {
82 88
     private let findJobsCTAHost = NSView()
83 89
     private let findJobsCTAChrome = HoverableView()
84 90
     private var findJobsCTAGradientLayer: CAGradientLayer?
85
-    private let welcomeSparkleIcon = NSImageView()
91
+    private let welcomeHeroHost = NSView()
92
+    private let welcomeHeroBackgroundView = WelcomeHeroBackgroundView()
93
+    private lazy var welcomeSparkleCluster: WelcomeSparkleClusterView = {
94
+        WelcomeSparkleClusterView(iconWell: Theme.welcomeHeroIconWell, tint: Theme.welcomeHeroHeadingBlue)
95
+    }()
86 96
     private let featureCardsRow = NSStackView()
87 97
     private let chatScrollView = NSScrollView()
88 98
     private let chatDocumentView = JobListingsDocumentView()
@@ -125,8 +135,6 @@ final class DashboardView: NSView, NSTextFieldDelegate {
125 135
         min(jobsPerSearchMaxCap, max(jobsPerSearchMin, requested))
126 136
     }
127 137
 
128
-    private static let welcomeSubtitleBreathKey = "welcomeSubtitleBreath"
129
-
130 138
     override init(frame frameRect: NSRect) {
131 139
         super.init(frame: frameRect)
132 140
         setupLayout()
@@ -207,36 +215,54 @@ final class DashboardView: NSView, NSTextFieldDelegate {
207 215
         mainOverlay.translatesAutoresizingMaskIntoConstraints = false
208 216
         mainOverlay.setContentHuggingPriority(.defaultLow, for: .vertical)
209 217
 
210
-        greetingLabel.font = .systemFont(ofSize: 32, weight: .bold)
211
-        greetingLabel.textColor = Theme.brandBlue
218
+        greetingLabel.font = .systemFont(ofSize: 34, weight: .bold)
219
+        greetingLabel.textColor = Theme.welcomeHeroHeadingBlue
212 220
         greetingLabel.alignment = .center
213 221
         greetingLabel.maximumNumberOfLines = 1
214 222
 
215 223
         subtitleLabel.font = .systemFont(ofSize: 15, weight: .regular)
216
-        subtitleLabel.textColor = Theme.welcomeSubtitleText
224
+        subtitleLabel.textColor = Theme.welcomeHeroSubtitleText
217 225
         subtitleLabel.alignment = .center
218 226
         subtitleLabel.maximumNumberOfLines = 2
219
-        subtitleLabel.wantsLayer = true
220 227
 
221 228
         let topInset = NSView()
222 229
         topInset.translatesAutoresizingMaskIntoConstraints = false
223
-        topInset.heightAnchor.constraint(equalToConstant: 18).isActive = true
230
+        topInset.heightAnchor.constraint(equalToConstant: 24).isActive = true
224 231
 
225 232
         configureSearchBar()
226 233
         configureChatViews()
227 234
 
228
-        welcomeSparkleIcon.translatesAutoresizingMaskIntoConstraints = false
229
-        welcomeSparkleIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 16, weight: .semibold)
230
-        welcomeSparkleIcon.image = NSImage(systemSymbolName: "sparkles", accessibilityDescription: nil)
231
-        welcomeSparkleIcon.contentTintColor = Theme.brandBlue
235
+        welcomeHeroHost.translatesAutoresizingMaskIntoConstraints = false
236
+        welcomeHeroBackgroundView.translatesAutoresizingMaskIntoConstraints = false
237
+        welcomeHeroBackgroundView.waveTint = Theme.welcomeHeroWaveTint
232 238
 
233
-        let titleBlock = NSStackView(views: [greetingLabel, subtitleLabel, welcomeSparkleIcon])
234
-        titleBlock.orientation = .vertical
235
-        titleBlock.spacing = 10
236
-        titleBlock.alignment = .centerX
239
+        let welcomeHeroContent = NSStackView(views: [welcomeSparkleCluster, greetingLabel, subtitleLabel])
240
+        welcomeHeroContent.orientation = .vertical
241
+        welcomeHeroContent.spacing = 14
242
+        welcomeHeroContent.alignment = .centerX
243
+        welcomeHeroContent.translatesAutoresizingMaskIntoConstraints = false
244
+
245
+        welcomeHeroHost.addSubview(welcomeHeroBackgroundView)
246
+        welcomeHeroHost.addSubview(welcomeHeroContent)
247
+        NSLayoutConstraint.activate([
248
+            welcomeHeroBackgroundView.leadingAnchor.constraint(equalTo: welcomeHeroHost.leadingAnchor),
249
+            welcomeHeroBackgroundView.trailingAnchor.constraint(equalTo: welcomeHeroHost.trailingAnchor),
250
+            welcomeHeroBackgroundView.topAnchor.constraint(equalTo: welcomeHeroHost.topAnchor),
251
+            welcomeHeroBackgroundView.bottomAnchor.constraint(equalTo: welcomeHeroHost.bottomAnchor),
252
+
253
+            welcomeHeroContent.centerXAnchor.constraint(equalTo: welcomeHeroHost.centerXAnchor),
254
+            welcomeHeroContent.leadingAnchor.constraint(greaterThanOrEqualTo: welcomeHeroHost.leadingAnchor, constant: 16),
255
+            welcomeHeroContent.trailingAnchor.constraint(lessThanOrEqualTo: welcomeHeroHost.trailingAnchor, constant: -16),
256
+            welcomeHeroContent.topAnchor.constraint(equalTo: welcomeHeroHost.topAnchor, constant: 8),
257
+            welcomeHeroContent.bottomAnchor.constraint(equalTo: welcomeHeroHost.bottomAnchor, constant: -4)
258
+        ])
237 259
 
238 260
         configureFeatureShortcutCards()
239 261
 
262
+        let heroCardsSpacer = NSView()
263
+        heroCardsSpacer.translatesAutoresizingMaskIntoConstraints = false
264
+        heroCardsSpacer.heightAnchor.constraint(equalToConstant: 36).isActive = true
265
+
240 266
         let midSpacer = NSView()
241 267
         midSpacer.translatesAutoresizingMaskIntoConstraints = false
242 268
         midSpacer.heightAnchor.constraint(equalToConstant: 12).isActive = true
@@ -250,7 +276,8 @@ final class DashboardView: NSView, NSTextFieldDelegate {
250 276
         chatBottomSpacer.heightAnchor.constraint(equalToConstant: 14).isActive = true
251 277
 
252 278
         mainOverlay.addArrangedSubview(topInset)
253
-        mainOverlay.addArrangedSubview(titleBlock)
279
+        mainOverlay.addArrangedSubview(welcomeHeroHost)
280
+        mainOverlay.addArrangedSubview(heroCardsSpacer)
254 281
         mainOverlay.addArrangedSubview(featureCardsRow)
255 282
         mainOverlay.addArrangedSubview(midSpacer)
256 283
         mainOverlay.addArrangedSubview(chatTopSpacer)
@@ -289,10 +316,15 @@ final class DashboardView: NSView, NSTextFieldDelegate {
289 316
             featureCardsRow.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
290 317
             chatScrollView.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
291 318
 
319
+            welcomeHeroHost.leadingAnchor.constraint(equalTo: mainOverlay.leadingAnchor),
320
+            welcomeHeroHost.trailingAnchor.constraint(equalTo: mainOverlay.trailingAnchor),
321
+
292 322
             greetingLabel.leadingAnchor.constraint(equalTo: mainOverlay.leadingAnchor, constant: 16),
293 323
             greetingLabel.trailingAnchor.constraint(equalTo: mainOverlay.trailingAnchor, constant: -16),
294 324
             subtitleLabel.leadingAnchor.constraint(equalTo: greetingLabel.leadingAnchor),
295
-            subtitleLabel.trailingAnchor.constraint(equalTo: greetingLabel.trailingAnchor)
325
+            subtitleLabel.trailingAnchor.constraint(equalTo: greetingLabel.trailingAnchor),
326
+
327
+            welcomeHeroContent.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, constant: -32)
296 328
         ])
297 329
     }
298 330
 
@@ -355,40 +387,10 @@ final class DashboardView: NSView, NSTextFieldDelegate {
355 387
         ])
356 388
     }
357 389
 
358
-    private func isWelcomeHeroVisible() -> Bool {
359
-        !mainOverlay.isHidden
360
-    }
361
-
362 390
     private var prefersReducedMotion: Bool {
363 391
         NSWorkspace.shared.accessibilityDisplayShouldReduceMotion
364 392
     }
365 393
 
366
-    private func syncWelcomeSubtitleBreathingAnimation() {
367
-        guard !prefersReducedMotion else {
368
-            stopWelcomeSubtitleBreathingAnimation()
369
-            return
370
-        }
371
-        guard isWelcomeHeroVisible(), !subtitleLabel.stringValue.isEmpty else {
372
-            stopWelcomeSubtitleBreathingAnimation()
373
-            return
374
-        }
375
-        guard let layer = subtitleLabel.layer, layer.animation(forKey: Self.welcomeSubtitleBreathKey) == nil else { return }
376
-
377
-        let pulse = CABasicAnimation(keyPath: "opacity")
378
-        pulse.fromValue = 1.0
379
-        pulse.toValue = 0.86
380
-        pulse.duration = 2.4
381
-        pulse.autoreverses = true
382
-        pulse.repeatCount = .greatestFiniteMagnitude
383
-        pulse.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
384
-        layer.add(pulse, forKey: Self.welcomeSubtitleBreathKey)
385
-    }
386
-
387
-    private func stopWelcomeSubtitleBreathingAnimation() {
388
-        subtitleLabel.layer?.removeAnimation(forKey: Self.welcomeSubtitleBreathKey)
389
-        subtitleLabel.layer?.opacity = 1
390
-    }
391
-
392 394
     private func updateJobListingDescriptionWidths() {
393 395
         updateDescriptionColumnWidths(in: savedJobsStack, containerWidth: savedJobsDocumentView.bounds.width)
394 396
         walkChatJobStacks { stack in
@@ -1303,11 +1305,6 @@ final class DashboardView: NSView, NSTextFieldDelegate {
1303 1305
                 nonHomeTitleLabel.stringValue = currentSidebarItems[selectedSidebarIndex].title
1304 1306
             }
1305 1307
         }
1306
-        if home {
1307
-            syncWelcomeSubtitleBreathingAnimation()
1308
-        } else {
1309
-            stopWelcomeSubtitleBreathingAnimation()
1310
-        }
1311 1308
     }
1312 1309
 
1313 1310
     /// Restores the main job-search experience: cleared query and a fresh chat history.
@@ -2467,6 +2464,135 @@ private struct OpenAIAPIErrorResponse: Codable {
2467 2464
     }
2468 2465
 }
2469 2466
 
2467
+/// Decorative waves and faint sparkles behind the welcome hero (reference layout).
2468
+private final class WelcomeHeroBackgroundView: NSView {
2469
+    /// Stroke color for side waves (pastel blue).
2470
+    var waveTint = NSColor(srgbRed: 186 / 255, green: 210 / 255, blue: 253 / 255, alpha: 1)
2471
+
2472
+    override var isFlipped: Bool { true }
2473
+
2474
+    override func draw(_ dirtyRect: NSRect) {
2475
+        NSColor.clear.setFill()
2476
+        bounds.fill()
2477
+
2478
+        guard bounds.width > 24, bounds.height > 24 else { return }
2479
+
2480
+        drawSideWaves(in: bounds, isLeft: true)
2481
+        drawSideWaves(in: bounds, isLeft: false)
2482
+        drawAmbientSparkles(in: bounds)
2483
+    }
2484
+
2485
+    private func drawSideWaves(in bounds: NSRect, isLeft: Bool) {
2486
+        for i in 0..<9 {
2487
+            let path = NSBezierPath()
2488
+            path.lineWidth = 1
2489
+            path.lineCapStyle = .round
2490
+            let phase = CGFloat(i) * 0.88
2491
+            let base = CGFloat(i + 1) * 11 + 4
2492
+            var first = true
2493
+            for y in stride(from: CGFloat(0), through: bounds.height, by: 2.8) {
2494
+                let wobble = sin(y * 0.048 + phase) * (4 + CGFloat(i % 5))
2495
+                let x = isLeft ? (base + wobble) : (bounds.width - base - wobble)
2496
+                let point = NSPoint(x: x, y: y)
2497
+                if first {
2498
+                    path.move(to: point)
2499
+                    first = false
2500
+                } else {
2501
+                    path.line(to: point)
2502
+                }
2503
+            }
2504
+            let fade = 1 - CGFloat(i) / 10
2505
+            waveTint.withAlphaComponent((0.09 + CGFloat(i % 3) * 0.022) * fade).setStroke()
2506
+            path.stroke()
2507
+        }
2508
+    }
2509
+
2510
+    private func drawAmbientSparkles(in bounds: NSRect) {
2511
+        let accent = NSColor(srgbRed: 0, green: 82 / 255, blue: 204 / 255, alpha: 1)
2512
+        let specs: [(CGFloat, CGFloat, CGFloat, CGFloat)] = [
2513
+            (0.06, 0.14, 9, 0.28),
2514
+            (0.94, 0.12, 12, 0.34),
2515
+            (0.18, 0.42, 5, 0.18),
2516
+            (0.86, 0.44, 6, 0.2),
2517
+            (0.5, 0.06, 7, 0.15)
2518
+        ]
2519
+        for (nx, ny, size, a) in specs {
2520
+            let center = NSPoint(x: bounds.width * nx, y: bounds.height * ny)
2521
+            fillFourPointStar(center: center, radius: size, color: accent.withAlphaComponent(a))
2522
+        }
2523
+    }
2524
+
2525
+    private func fillFourPointStar(center: NSPoint, radius: CGFloat, color: NSColor) {
2526
+        let path = NSBezierPath()
2527
+        for i in 0..<4 {
2528
+            let angle = CGFloat(i) * .pi / 2 - .pi / 2
2529
+            let x = center.x + cos(angle) * radius
2530
+            let y = center.y + sin(angle) * radius
2531
+            let point = NSPoint(x: x, y: y)
2532
+            if i == 0 {
2533
+                path.move(to: point)
2534
+            } else {
2535
+                path.line(to: point)
2536
+            }
2537
+        }
2538
+        path.close()
2539
+        color.setFill()
2540
+        path.fill()
2541
+    }
2542
+}
2543
+
2544
+/// Circular pastel well with three sparkle symbols (reference: layered stars of different sizes).
2545
+private final class WelcomeSparkleClusterView: NSView {
2546
+    private let diameter: CGFloat = 72
2547
+
2548
+    override var intrinsicContentSize: NSSize {
2549
+        NSSize(width: diameter, height: diameter)
2550
+    }
2551
+
2552
+    init(iconWell: NSColor, tint: NSColor) {
2553
+        super.init(frame: .zero)
2554
+        translatesAutoresizingMaskIntoConstraints = false
2555
+        wantsLayer = true
2556
+        layer?.cornerRadius = diameter / 2
2557
+        if #available(macOS 11.0, *) {
2558
+            layer?.cornerCurve = .continuous
2559
+        }
2560
+        layer?.backgroundColor = iconWell.cgColor
2561
+
2562
+        let configs: [(CGFloat, NSFont.Weight, CGFloat, CGFloat)] = [
2563
+            (22, .medium, 0, 0),
2564
+            (14, .regular, -14, -11),
2565
+            (11, .regular, 15, 13)
2566
+        ]
2567
+
2568
+        let symbolName = Self.sparkleSymbolName()
2569
+        for (size, weight, ox, oy) in configs {
2570
+            let iv = NSImageView()
2571
+            iv.translatesAutoresizingMaskIntoConstraints = false
2572
+            iv.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: size, weight: weight)
2573
+            iv.image = NSImage(systemSymbolName: symbolName, accessibilityDescription: nil)
2574
+            iv.contentTintColor = tint
2575
+            addSubview(iv)
2576
+            NSLayoutConstraint.activate([
2577
+                iv.centerXAnchor.constraint(equalTo: centerXAnchor, constant: ox),
2578
+                iv.centerYAnchor.constraint(equalTo: centerYAnchor, constant: oy)
2579
+            ])
2580
+        }
2581
+    }
2582
+
2583
+    private static func sparkleSymbolName() -> String {
2584
+        if NSImage(systemSymbolName: "sparkle", accessibilityDescription: nil) != nil {
2585
+            return "sparkle"
2586
+        }
2587
+        return "sparkles"
2588
+    }
2589
+
2590
+    @available(*, unavailable)
2591
+    required init?(coder: NSCoder) {
2592
+        fatalError("init(coder:) has not been implemented")
2593
+    }
2594
+}
2595
+
2470 2596
 /// Home welcome row: three tappable shortcuts that seed the main search field (reference: white cards, pastel icon well, arrow at bottom trailing).
2471 2597
 private final class FeatureShortcutCardView: NSView {
2472 2598
     private static let cardCornerRadius: CGFloat = 14
@@ -2493,8 +2619,8 @@ private final class FeatureShortcutCardView: NSView {
2493 2619
         layer?.shadowRadius = 12
2494 2620
         layer?.shadowOpacity = 1
2495 2621
 
2496
-        // `#0047AB` — primary title / icons / arrow.
2497
-        let primaryBlue = NSColor(srgbRed: 0 / 255, green: 71 / 255, blue: 171 / 255, alpha: 1)
2622
+        // `#0052CC` — primary title / icons / arrow (matches welcome hero reference).
2623
+        let primaryBlue = NSColor(srgbRed: 0, green: 82 / 255, blue: 204 / 255, alpha: 1)
2498 2624
         // `#EBF2FF` — circular icon well.
2499 2625
         let iconWellColor = NSColor(srgbRed: 235 / 255, green: 242 / 255, blue: 255 / 255, alpha: 1)
2500 2626
         // `#5D6D7E` — muted description.