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

Add dashboard UI architecture and connect main window flow.

Introduce dashboard models, views, and controller wiring, update the storyboard launch behavior, and switch the main view controller to the new dashboard implementation.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 недель назад: 3
Родитель
Сommit
7d11c65379

+ 1 - 1
App for Indeed/Base.lproj/Main.storyboard

@@ -683,7 +683,7 @@
683 683
         <scene sceneID="R2V-B0-nI4">
684 684
             <objects>
685 685
                 <windowController id="B8D-0N-5wS" sceneMemberID="viewController">
686
-                    <window key="window" title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="IQv-IB-iLA">
686
+                    <window key="window" title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="YES" animationBehavior="default" id="IQv-IB-iLA">
687 687
                         <windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
688 688
                         <windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
689 689
                         <rect key="contentRect" x="196" y="240" width="480" height="270"/>

+ 32 - 0
App for Indeed/Controllers/DashboardViewController.swift

@@ -0,0 +1,32 @@
1
+//
2
+//  DashboardViewController.swift
3
+//  App for Indeed
4
+//
5
+
6
+import Cocoa
7
+
8
+class DashboardViewController: NSViewController {
9
+    private let dataProvider: DashboardDataProviding
10
+    private let dashboardView = DashboardView(frame: .zero)
11
+
12
+    init(dataProvider: DashboardDataProviding = MockDashboardDataProvider()) {
13
+        self.dataProvider = dataProvider
14
+        super.init(nibName: nil, bundle: nil)
15
+    }
16
+
17
+    @available(*, unavailable)
18
+    required init?(coder: NSCoder) {
19
+        self.dataProvider = MockDashboardDataProvider()
20
+        super.init(coder: coder)
21
+    }
22
+
23
+    override func loadView() {
24
+        view = dashboardView
25
+    }
26
+
27
+    override func viewDidLoad() {
28
+        super.viewDidLoad()
29
+        let data = dataProvider.loadDashboardData()
30
+        dashboardView.render(data)
31
+    }
32
+}

+ 77 - 0
App for Indeed/Models/DashboardModels.swift

@@ -0,0 +1,77 @@
1
+//
2
+//  DashboardModels.swift
3
+//  App for Indeed
4
+//
5
+
6
+import Foundation
7
+
8
+struct SidebarItem {
9
+    let title: String
10
+    let systemImage: String
11
+    let badge: String?
12
+}
13
+
14
+struct StatCard {
15
+    let title: String
16
+    let value: String
17
+    let trend: String
18
+}
19
+
20
+struct JobRecommendation {
21
+    let title: String
22
+    let company: String
23
+    let location: String
24
+    let matchRate: String
25
+    let postedAgo: String
26
+}
27
+
28
+struct InsightItem {
29
+    let title: String
30
+    let description: String
31
+    let systemImage: String
32
+}
33
+
34
+struct DashboardData {
35
+    let greetingName: String
36
+    let sidebarItems: [SidebarItem]
37
+    let stats: [StatCard]
38
+    let recommendations: [JobRecommendation]
39
+    let insights: [InsightItem]
40
+}
41
+
42
+protocol DashboardDataProviding {
43
+    func loadDashboardData() -> DashboardData
44
+}
45
+
46
+final class MockDashboardDataProvider: DashboardDataProviding {
47
+    func loadDashboardData() -> DashboardData {
48
+        DashboardData(
49
+            greetingName: "Olivia",
50
+            sidebarItems: [
51
+                SidebarItem(title: "Overview", systemImage: "house.fill", badge: nil),
52
+                SidebarItem(title: "AI Job Search", systemImage: "wand.and.stars", badge: "New"),
53
+                SidebarItem(title: "My Jobs", systemImage: "bookmark", badge: nil),
54
+                SidebarItem(title: "Saved Jobs", systemImage: "heart", badge: nil),
55
+                SidebarItem(title: "Applications", systemImage: "doc.text", badge: nil),
56
+                SidebarItem(title: "Profile", systemImage: "person", badge: nil),
57
+                SidebarItem(title: "Settings", systemImage: "gearshape", badge: nil)
58
+            ],
59
+            stats: [
60
+                StatCard(title: "Jobs Found", value: "128", trend: "+24 this week"),
61
+                StatCard(title: "Saved Jobs", value: "32", trend: "+6 this week"),
62
+                StatCard(title: "Applications", value: "14", trend: "+3 this week"),
63
+                StatCard(title: "Interviews", value: "5", trend: "Upcoming")
64
+            ],
65
+            recommendations: [
66
+                JobRecommendation(title: "Senior Product Designer", company: "Dropbox", location: "San Francisco, CA", matchRate: "95% Match", postedAgo: "2h ago"),
67
+                JobRecommendation(title: "UX Researcher", company: "Airbnb", location: "Remote", matchRate: "90% Match", postedAgo: "4h ago"),
68
+                JobRecommendation(title: "Product Manager", company: "Stripe", location: "New York, NY", matchRate: "88% Match", postedAgo: "6h ago")
69
+            ],
70
+            insights: [
71
+                InsightItem(title: "High Demand", description: "Product designer roles are in high demand right now.", systemImage: "chart.line.uptrend.xyaxis"),
72
+                InsightItem(title: "Improve Your Profile", description: "Add more skills to increase your match rate.", systemImage: "person.crop.circle.badge.checkmark"),
73
+                InsightItem(title: "New Opportunities", description: "24 new jobs match your profile this week.", systemImage: "briefcase.fill")
74
+            ]
75
+        )
76
+    }
77
+}

+ 1 - 16
App for Indeed/ViewController.swift

@@ -7,20 +7,5 @@
7 7
 
8 8
 import Cocoa
9 9
 
10
-class ViewController: NSViewController {
11
-
12
-    override func viewDidLoad() {
13
-        super.viewDidLoad()
14
-
15
-        // Do any additional setup after loading the view.
16
-    }
17
-
18
-    override var representedObject: Any? {
19
-        didSet {
20
-        // Update the view, if already loaded.
21
-        }
22
-    }
23
-
24
-
25
-}
10
+final class ViewController: DashboardViewController {}
26 11
 

+ 414 - 0
App for Indeed/Views/DashboardView.swift

@@ -0,0 +1,414 @@
1
+//
2
+//  DashboardView.swift
3
+//  App for Indeed
4
+//
5
+
6
+import Cocoa
7
+
8
+final class DashboardView: NSView {
9
+    private let contentStack = NSStackView()
10
+    private let documentContainer = NSView()
11
+    private let sidebar = NSStackView()
12
+    private let mainColumn = NSStackView()
13
+    private let greetingLabel = NSTextField(labelWithString: "")
14
+    private let subtitleLabel = NSTextField(labelWithString: "Find your perfect job with the power of AI.")
15
+    private let heroCard = NSView()
16
+    private let statGrid = NSGridView(views: [[]])
17
+    private let recommendationsStack = NSStackView()
18
+    private let insightsStack = NSStackView()
19
+    private let scrollView = NSScrollView()
20
+
21
+    override init(frame frameRect: NSRect) {
22
+        super.init(frame: frameRect)
23
+        setupLayout()
24
+    }
25
+
26
+    required init?(coder: NSCoder) {
27
+        super.init(coder: coder)
28
+        setupLayout()
29
+    }
30
+
31
+    override func layout() {
32
+        super.layout()
33
+        updateDocumentLayout()
34
+    }
35
+
36
+    func render(_ data: DashboardData) {
37
+        greetingLabel.stringValue = "Welcome back, \(data.greetingName)! 👋"
38
+        configureSidebar(data.sidebarItems)
39
+        configureStats(data.stats)
40
+        configureRecommendations(data.recommendations)
41
+        configureInsights(data.insights)
42
+        updateDocumentLayout()
43
+    }
44
+
45
+    private func setupLayout() {
46
+        wantsLayer = true
47
+        layer?.backgroundColor = NSColor(calibratedRed: 0.03, green: 0.06, blue: 0.14, alpha: 1).cgColor
48
+
49
+        scrollView.translatesAutoresizingMaskIntoConstraints = false
50
+        scrollView.hasVerticalScroller = true
51
+        scrollView.drawsBackground = false
52
+        addSubview(scrollView)
53
+
54
+        contentStack.orientation = .horizontal
55
+        contentStack.spacing = 20
56
+        contentStack.translatesAutoresizingMaskIntoConstraints = false
57
+        contentStack.edgeInsets = NSEdgeInsets(top: 20, left: 20, bottom: 24, right: 20)
58
+
59
+        documentContainer.translatesAutoresizingMaskIntoConstraints = true
60
+        documentContainer.autoresizingMask = [.width]
61
+        documentContainer.frame = NSRect(x: 0, y: 0, width: 1040, height: 900)
62
+        documentContainer.addSubview(contentStack)
63
+        scrollView.documentView = documentContainer
64
+
65
+        sidebar.orientation = .vertical
66
+        sidebar.spacing = 10
67
+        sidebar.alignment = .leading
68
+        sidebar.wantsLayer = true
69
+        sidebar.layer?.backgroundColor = NSColor(calibratedRed: 0.06, green: 0.09, blue: 0.2, alpha: 1).cgColor
70
+        sidebar.layer?.cornerRadius = 16
71
+        sidebar.edgeInsets = NSEdgeInsets(top: 18, left: 14, bottom: 18, right: 14)
72
+        sidebar.translatesAutoresizingMaskIntoConstraints = false
73
+
74
+        mainColumn.orientation = .vertical
75
+        mainColumn.spacing = 14
76
+        mainColumn.alignment = .leading
77
+        mainColumn.translatesAutoresizingMaskIntoConstraints = false
78
+
79
+        greetingLabel.font = .systemFont(ofSize: 30, weight: .bold)
80
+        greetingLabel.textColor = .white
81
+        subtitleLabel.font = .systemFont(ofSize: 14, weight: .regular)
82
+        subtitleLabel.textColor = NSColor.white.withAlphaComponent(0.75)
83
+
84
+        heroCard.wantsLayer = true
85
+        heroCard.layer?.backgroundColor = NSColor(calibratedRed: 0.08, green: 0.11, blue: 0.28, alpha: 1).cgColor
86
+        heroCard.layer?.cornerRadius = 18
87
+        heroCard.translatesAutoresizingMaskIntoConstraints = false
88
+        let hero = buildHeroContent()
89
+        heroCard.addSubview(hero)
90
+
91
+        statGrid.translatesAutoresizingMaskIntoConstraints = false
92
+        statGrid.rowSpacing = 10
93
+        statGrid.columnSpacing = 10
94
+
95
+        let lowerSection = NSStackView()
96
+        lowerSection.orientation = .horizontal
97
+        lowerSection.spacing = 12
98
+        lowerSection.alignment = .top
99
+        lowerSection.translatesAutoresizingMaskIntoConstraints = false
100
+
101
+        let recommendationsBox = sectionBox(title: "Recommended for You", content: recommendationsStack)
102
+        let insightsBox = sectionBox(title: "AI Insights", content: insightsStack)
103
+        recommendationsBox.translatesAutoresizingMaskIntoConstraints = false
104
+        insightsBox.translatesAutoresizingMaskIntoConstraints = false
105
+
106
+        lowerSection.addArrangedSubview(recommendationsBox)
107
+        lowerSection.addArrangedSubview(insightsBox)
108
+
109
+        mainColumn.addArrangedSubview(greetingLabel)
110
+        mainColumn.addArrangedSubview(subtitleLabel)
111
+        mainColumn.addArrangedSubview(heroCard)
112
+        mainColumn.addArrangedSubview(statGrid)
113
+        mainColumn.addArrangedSubview(lowerSection)
114
+
115
+        contentStack.addArrangedSubview(sidebar)
116
+        contentStack.addArrangedSubview(mainColumn)
117
+
118
+        NSLayoutConstraint.activate([
119
+            scrollView.leadingAnchor.constraint(equalTo: leadingAnchor),
120
+            scrollView.trailingAnchor.constraint(equalTo: trailingAnchor),
121
+            scrollView.topAnchor.constraint(equalTo: topAnchor),
122
+            scrollView.bottomAnchor.constraint(equalTo: bottomAnchor),
123
+
124
+            contentStack.leadingAnchor.constraint(equalTo: documentContainer.leadingAnchor),
125
+            contentStack.trailingAnchor.constraint(equalTo: documentContainer.trailingAnchor),
126
+            contentStack.topAnchor.constraint(equalTo: documentContainer.topAnchor),
127
+            contentStack.bottomAnchor.constraint(equalTo: documentContainer.bottomAnchor),
128
+            contentStack.widthAnchor.constraint(equalTo: documentContainer.widthAnchor),
129
+
130
+            sidebar.widthAnchor.constraint(equalToConstant: 225),
131
+            mainColumn.widthAnchor.constraint(greaterThanOrEqualToConstant: 760),
132
+            heroCard.widthAnchor.constraint(equalTo: mainColumn.widthAnchor),
133
+            heroCard.heightAnchor.constraint(equalToConstant: 140),
134
+
135
+            hero.leadingAnchor.constraint(equalTo: heroCard.leadingAnchor, constant: 22),
136
+            hero.trailingAnchor.constraint(equalTo: heroCard.trailingAnchor, constant: -22),
137
+            hero.topAnchor.constraint(equalTo: heroCard.topAnchor, constant: 18),
138
+            hero.bottomAnchor.constraint(equalTo: heroCard.bottomAnchor, constant: -18),
139
+
140
+            recommendationsBox.widthAnchor.constraint(equalToConstant: 510),
141
+            insightsBox.widthAnchor.constraint(equalToConstant: 238)
142
+        ])
143
+    }
144
+
145
+    private func buildHeroContent() -> NSView {
146
+        let container = NSStackView()
147
+        container.orientation = .horizontal
148
+        container.translatesAutoresizingMaskIntoConstraints = false
149
+        container.distribution = .fillEqually
150
+
151
+        let left = NSStackView()
152
+        left.orientation = .vertical
153
+        left.spacing = 8
154
+        left.alignment = .leading
155
+
156
+        let title = NSTextField(labelWithString: "AI Job Search Assistant")
157
+        title.font = .systemFont(ofSize: 24, weight: .semibold)
158
+        title.textColor = .white
159
+
160
+        let body = NSTextField(labelWithString: "Let AI find the best jobs for you on Indeed based on your preferences.")
161
+        body.font = .systemFont(ofSize: 12, weight: .regular)
162
+        body.textColor = NSColor.white.withAlphaComponent(0.72)
163
+
164
+        let action = NSButton(title: "Find Jobs with AI", target: nil, action: nil)
165
+        action.bezelStyle = .rounded
166
+        action.wantsLayer = true
167
+        action.layer?.backgroundColor = NSColor(calibratedRed: 0.27, green: 0.42, blue: 1.0, alpha: 1).cgColor
168
+        action.layer?.cornerRadius = 8
169
+        action.contentTintColor = .white
170
+        action.font = .systemFont(ofSize: 13, weight: .semibold)
171
+
172
+        left.addArrangedSubview(title)
173
+        left.addArrangedSubview(body)
174
+        left.addArrangedSubview(action)
175
+
176
+        let right = NSStackView()
177
+        right.orientation = .vertical
178
+        right.alignment = .trailing
179
+        right.addArrangedSubview(tagBubble("Quick"))
180
+        right.addArrangedSubview(tagBubble("Smart"))
181
+        right.addArrangedSubview(tagBubble("Personalized"))
182
+
183
+        container.addArrangedSubview(left)
184
+        container.addArrangedSubview(right)
185
+        return container
186
+    }
187
+
188
+    private func configureSidebar(_ items: [SidebarItem]) {
189
+        sidebar.arrangedSubviews.forEach {
190
+            sidebar.removeArrangedSubview($0)
191
+            $0.removeFromSuperview()
192
+        }
193
+
194
+        let brand = NSTextField(labelWithString: "Indeed AI\nJob Finder")
195
+        brand.font = .systemFont(ofSize: 18, weight: .bold)
196
+        brand.textColor = .white
197
+        sidebar.addArrangedSubview(brand)
198
+
199
+        let spacer = NSView()
200
+        spacer.translatesAutoresizingMaskIntoConstraints = false
201
+        spacer.heightAnchor.constraint(equalToConstant: 8).isActive = true
202
+        sidebar.addArrangedSubview(spacer)
203
+
204
+        items.enumerated().forEach { index, item in
205
+            let row = NSStackView()
206
+            row.orientation = .horizontal
207
+            row.spacing = 8
208
+            row.alignment = .centerY
209
+            row.wantsLayer = true
210
+            row.layer?.cornerRadius = 8
211
+            row.edgeInsets = NSEdgeInsets(top: 8, left: 10, bottom: 8, right: 10)
212
+            if index == 0 {
213
+                row.layer?.backgroundColor = NSColor(calibratedRed: 0.22, green: 0.31, blue: 0.85, alpha: 0.4).cgColor
214
+            }
215
+
216
+            let icon = NSImageView()
217
+            icon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 13, weight: .medium)
218
+            icon.image = NSImage(systemSymbolName: item.systemImage, accessibilityDescription: item.title)
219
+            icon.contentTintColor = .white
220
+
221
+            let text = NSTextField(labelWithString: item.title)
222
+            text.font = .systemFont(ofSize: 14, weight: .medium)
223
+            text.textColor = .white
224
+
225
+            row.addArrangedSubview(icon)
226
+            row.addArrangedSubview(text)
227
+
228
+            if let badge = item.badge {
229
+                let badgeField = NSTextField(labelWithString: badge)
230
+                badgeField.font = .systemFont(ofSize: 11, weight: .semibold)
231
+                badgeField.textColor = .white
232
+                badgeField.wantsLayer = true
233
+                badgeField.layer?.backgroundColor = NSColor.systemPurple.cgColor
234
+                badgeField.layer?.cornerRadius = 8
235
+                badgeField.alignment = .center
236
+                badgeField.maximumNumberOfLines = 1
237
+                badgeField.lineBreakMode = .byClipping
238
+                badgeField.translatesAutoresizingMaskIntoConstraints = false
239
+                badgeField.widthAnchor.constraint(equalToConstant: 42).isActive = true
240
+                row.addArrangedSubview(NSView())
241
+                row.addArrangedSubview(badgeField)
242
+            }
243
+            sidebar.addArrangedSubview(row)
244
+        }
245
+    }
246
+
247
+    private func configureStats(_ stats: [StatCard]) {
248
+        statGrid.removeRow(at: 0)
249
+        let cards = stats.map { stat -> NSView in
250
+            let card = NSStackView()
251
+            card.orientation = .vertical
252
+            card.spacing = 6
253
+            card.edgeInsets = NSEdgeInsets(top: 14, left: 14, bottom: 14, right: 14)
254
+            card.wantsLayer = true
255
+            card.layer?.backgroundColor = NSColor(calibratedRed: 0.06, green: 0.11, blue: 0.24, alpha: 1).cgColor
256
+            card.layer?.cornerRadius = 14
257
+
258
+            let value = NSTextField(labelWithString: stat.value)
259
+            value.font = .systemFont(ofSize: 30, weight: .bold)
260
+            value.textColor = .white
261
+
262
+            let title = NSTextField(labelWithString: stat.title)
263
+            title.font = .systemFont(ofSize: 13, weight: .medium)
264
+            title.textColor = NSColor.white.withAlphaComponent(0.74)
265
+
266
+            let trend = NSTextField(labelWithString: stat.trend)
267
+            trend.font = .systemFont(ofSize: 12, weight: .semibold)
268
+            trend.textColor = NSColor.systemGreen
269
+
270
+            card.addArrangedSubview(value)
271
+            card.addArrangedSubview(title)
272
+            card.addArrangedSubview(trend)
273
+            card.translatesAutoresizingMaskIntoConstraints = false
274
+            card.widthAnchor.constraint(equalToConstant: 185).isActive = true
275
+            card.heightAnchor.constraint(equalToConstant: 120).isActive = true
276
+            return card
277
+        }
278
+        statGrid.addRow(with: cards)
279
+    }
280
+
281
+    private func configureRecommendations(_ recommendations: [JobRecommendation]) {
282
+        recommendationsStack.orientation = .vertical
283
+        recommendationsStack.spacing = 10
284
+        recommendationsStack.alignment = .leading
285
+        recommendationsStack.arrangedSubviews.forEach {
286
+            recommendationsStack.removeArrangedSubview($0)
287
+            $0.removeFromSuperview()
288
+        }
289
+
290
+        recommendations.forEach { recommendation in
291
+            let row = NSStackView()
292
+            row.orientation = .horizontal
293
+            row.spacing = 12
294
+            row.alignment = .centerY
295
+
296
+            let icon = NSView()
297
+            icon.wantsLayer = true
298
+            icon.layer?.cornerRadius = 10
299
+            icon.layer?.backgroundColor = NSColor(calibratedRed: 0.19, green: 0.34, blue: 0.9, alpha: 0.9).cgColor
300
+            icon.translatesAutoresizingMaskIntoConstraints = false
301
+            icon.widthAnchor.constraint(equalToConstant: 38).isActive = true
302
+            icon.heightAnchor.constraint(equalToConstant: 38).isActive = true
303
+
304
+            let textColumn = NSStackView()
305
+            textColumn.orientation = .vertical
306
+            textColumn.spacing = 2
307
+            textColumn.alignment = .leading
308
+
309
+            let title = NSTextField(labelWithString: recommendation.title)
310
+            title.font = .systemFont(ofSize: 15, weight: .semibold)
311
+            title.textColor = .white
312
+
313
+            let subtitle = NSTextField(labelWithString: "\(recommendation.company) • \(recommendation.location)")
314
+            subtitle.font = .systemFont(ofSize: 12, weight: .regular)
315
+            subtitle.textColor = NSColor.white.withAlphaComponent(0.7)
316
+
317
+            textColumn.addArrangedSubview(title)
318
+            textColumn.addArrangedSubview(subtitle)
319
+
320
+            let meta = NSStackView()
321
+            meta.orientation = .vertical
322
+            meta.alignment = .trailing
323
+
324
+            let match = NSTextField(labelWithString: recommendation.matchRate)
325
+            match.font = .systemFont(ofSize: 12, weight: .semibold)
326
+            match.textColor = NSColor.systemGreen
327
+
328
+            let posted = NSTextField(labelWithString: recommendation.postedAgo)
329
+            posted.font = .systemFont(ofSize: 11, weight: .regular)
330
+            posted.textColor = NSColor.white.withAlphaComponent(0.6)
331
+
332
+            meta.addArrangedSubview(match)
333
+            meta.addArrangedSubview(posted)
334
+
335
+            row.addArrangedSubview(icon)
336
+            row.addArrangedSubview(textColumn)
337
+            row.addArrangedSubview(NSView())
338
+            row.addArrangedSubview(meta)
339
+
340
+            recommendationsStack.addArrangedSubview(row)
341
+        }
342
+    }
343
+
344
+    private func configureInsights(_ insights: [InsightItem]) {
345
+        insightsStack.orientation = .vertical
346
+        insightsStack.spacing = 12
347
+        insightsStack.alignment = .leading
348
+        insightsStack.arrangedSubviews.forEach {
349
+            insightsStack.removeArrangedSubview($0)
350
+            $0.removeFromSuperview()
351
+        }
352
+
353
+        insights.forEach { insight in
354
+            let card = NSStackView()
355
+            card.orientation = .vertical
356
+            card.spacing = 4
357
+            card.edgeInsets = NSEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
358
+            card.wantsLayer = true
359
+            card.layer?.backgroundColor = NSColor(calibratedRed: 0.07, green: 0.12, blue: 0.22, alpha: 1).cgColor
360
+            card.layer?.cornerRadius = 10
361
+
362
+            let title = NSTextField(labelWithString: insight.title)
363
+            title.font = .systemFont(ofSize: 14, weight: .semibold)
364
+            title.textColor = .white
365
+
366
+            let body = NSTextField(labelWithString: insight.description)
367
+            body.font = .systemFont(ofSize: 12, weight: .regular)
368
+            body.textColor = NSColor.white.withAlphaComponent(0.7)
369
+            body.maximumNumberOfLines = 2
370
+            body.lineBreakMode = .byWordWrapping
371
+
372
+            card.addArrangedSubview(title)
373
+            card.addArrangedSubview(body)
374
+            insightsStack.addArrangedSubview(card)
375
+        }
376
+    }
377
+
378
+    private func sectionBox(title: String, content: NSStackView) -> NSView {
379
+        let box = NSStackView()
380
+        box.orientation = .vertical
381
+        box.spacing = 12
382
+        box.alignment = .leading
383
+        box.edgeInsets = NSEdgeInsets(top: 14, left: 14, bottom: 14, right: 14)
384
+        box.wantsLayer = true
385
+        box.layer?.backgroundColor = NSColor(calibratedRed: 0.05, green: 0.09, blue: 0.19, alpha: 1).cgColor
386
+        box.layer?.cornerRadius = 16
387
+
388
+        let titleLabel = NSTextField(labelWithString: title)
389
+        titleLabel.font = .systemFont(ofSize: 18, weight: .semibold)
390
+        titleLabel.textColor = .white
391
+        box.addArrangedSubview(titleLabel)
392
+        box.addArrangedSubview(content)
393
+        return box
394
+    }
395
+
396
+    private func tagBubble(_ text: String) -> NSView {
397
+        let label = NSTextField(labelWithString: text)
398
+        label.font = .systemFont(ofSize: 11, weight: .medium)
399
+        label.textColor = NSColor(calibratedRed: 0.75, green: 0.79, blue: 1, alpha: 1)
400
+        label.wantsLayer = true
401
+        label.layer?.backgroundColor = NSColor(calibratedRed: 0.2, green: 0.24, blue: 0.45, alpha: 0.5).cgColor
402
+        label.layer?.cornerRadius = 7
403
+        label.alignment = .center
404
+        label.translatesAutoresizingMaskIntoConstraints = false
405
+        label.widthAnchor.constraint(equalToConstant: 90).isActive = true
406
+        return label
407
+    }
408
+
409
+    private func updateDocumentLayout() {
410
+        documentContainer.layoutSubtreeIfNeeded()
411
+        let fittingHeight = max(contentStack.fittingSize.height, bounds.height)
412
+        documentContainer.frame = NSRect(x: 0, y: 0, width: bounds.width, height: fittingHeight)
413
+    }
414
+}