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

Apply dark theme to Profile hub, editor, and CV preview.

Wire profiles list, profile editor, and filled CV preview chrome to AppDashboardTheme so they match the dashboard when appearance changes.

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

+ 12 - 0
App for Indeed/Services/AppDashboardTheme.swift

@@ -287,4 +287,16 @@ enum AppDashboardTheme {
287
             ? NSColor(srgbRed: 58 / 255, green: 58 / 255, blue: 60 / 255, alpha: 1)
287
             ? NSColor(srgbRed: 58 / 255, green: 58 / 255, blue: 60 / 255, alpha: 1)
288
             : NSColor.white.withAlphaComponent(0.65)
288
             : NSColor.white.withAlphaComponent(0.65)
289
     }
289
     }
290
+
291
+    // MARK: Profile hub & editor
292
+
293
+    static var profileFieldFill: NSColor {
294
+        isDark
295
+            ? NSColor(srgbRed: 52 / 255, green: 52 / 255, blue: 54 / 255, alpha: 1)
296
+            : NSColor(srgbRed: 247 / 255, green: 247 / 255, blue: 247 / 255, alpha: 1)
297
+    }
298
+
299
+    static var profileDestructive: NSColor {
300
+        NSColor(srgbRed: 220 / 255, green: 38 / 255, blue: 38 / 255, alpha: 1)
301
+    }
290
 }
302
 }

+ 51 - 17
App for Indeed/Views/CVFilledPreviewPageView.swift

@@ -14,8 +14,9 @@ private final class CVPreviewFlippedDocumentView: NSView {
14
 
14
 
15
 /// Same metrics as job cards’ **Apply** (`DashboardView` `JobPayloadButton`): 13pt semibold, 32pt tall, 8pt corners.
15
 /// Same metrics as job cards’ **Apply** (`DashboardView` `JobPayloadButton`): 13pt semibold, 32pt tall, 8pt corners.
16
 private final class CVPreviewPrimaryCTAButton: NSButton {
16
 private final class CVPreviewPrimaryCTAButton: NSButton {
17
-    private static let fill = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 1)
18
-    private static let fillHover = NSColor(srgbRed: 28 / 255, green: 70 / 255, blue: 140 / 255, alpha: 1)
17
+    private static var fill: NSColor { AppDashboardTheme.brandBlue }
18
+    private static var fillHover: NSColor { AppDashboardTheme.brandBlueHover }
19
+    private static var labelColor: NSColor { AppDashboardTheme.proCTAText }
19
     /// Slightly wider than default title metrics so the label is not flush to the pill edges.
20
     /// Slightly wider than default title metrics so the label is not flush to the pill edges.
20
     private static let horizontalOutset: CGFloat = 20
21
     private static let horizontalOutset: CGFloat = 20
21
 
22
 
@@ -34,7 +35,7 @@ private final class CVPreviewPrimaryCTAButton: NSButton {
34
         wantsLayer = true
35
         wantsLayer = true
35
         layer?.cornerRadius = 8
36
         layer?.cornerRadius = 8
36
         layer?.backgroundColor = Self.fill.cgColor
37
         layer?.backgroundColor = Self.fill.cgColor
37
-        contentTintColor = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
38
+        contentTintColor = Self.labelColor
38
         focusRingType = .none
39
         focusRingType = .none
39
         setContentHuggingPriority(.required, for: .horizontal)
40
         setContentHuggingPriority(.required, for: .horizontal)
40
         setContentCompressionResistancePriority(.required, for: .horizontal)
41
         setContentCompressionResistancePriority(.required, for: .horizontal)
@@ -64,9 +65,17 @@ private final class CVPreviewPrimaryCTAButton: NSButton {
64
         trackingAreaRef = ta
65
         trackingAreaRef = ta
65
     }
66
     }
66
 
67
 
68
+    func applyCurrentAppearance() {
69
+        contentTintColor = Self.labelColor
70
+        layer?.backgroundColor = (isHovering ? Self.fillHover : Self.fill).cgColor
71
+    }
72
+
73
+    private var isHovering = false
74
+
67
     override func mouseEntered(with event: NSEvent) {
75
     override func mouseEntered(with event: NSEvent) {
68
         super.mouseEntered(with: event)
76
         super.mouseEntered(with: event)
69
-        layer?.backgroundColor = Self.fillHover.cgColor
77
+        isHovering = true
78
+        applyCurrentAppearance()
70
         if !didPushCursor {
79
         if !didPushCursor {
71
             NSCursor.pointingHand.push()
80
             NSCursor.pointingHand.push()
72
             didPushCursor = true
81
             didPushCursor = true
@@ -75,7 +84,8 @@ private final class CVPreviewPrimaryCTAButton: NSButton {
75
 
84
 
76
     override func mouseExited(with event: NSEvent) {
85
     override func mouseExited(with event: NSEvent) {
77
         super.mouseExited(with: event)
86
         super.mouseExited(with: event)
78
-        layer?.backgroundColor = Self.fill.cgColor
87
+        isHovering = false
88
+        applyCurrentAppearance()
79
         if didPushCursor {
89
         if didPushCursor {
80
             NSCursor.pop()
90
             NSCursor.pop()
81
             didPushCursor = false
91
             didPushCursor = false
@@ -107,41 +117,37 @@ final class CVFilledPreviewPageView: NSView {
107
     private var lastProfile: SavedProfile?
117
     private var lastProfile: SavedProfile?
108
     private var lastTemplate: CVTemplate?
118
     private var lastTemplate: CVTemplate?
109
 
119
 
110
-    private static let pageBackground = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
111
-    private static let secondaryText = NSColor(srgbRed: 100 / 255, green: 116 / 255, blue: 139 / 255, alpha: 1)
112
-    private static let brandBlue = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 1)
120
+    private let subtitleLabel = NSTextField(wrappingLabelWithString: "")
121
+    private var appearanceObserver: NSObjectProtocol?
113
 
122
 
114
     override init(frame frameRect: NSRect) {
123
     override init(frame frameRect: NSRect) {
115
         super.init(frame: frameRect)
124
         super.init(frame: frameRect)
116
         wantsLayer = true
125
         wantsLayer = true
117
-        layer?.backgroundColor = Self.pageBackground.cgColor
118
         userInterfaceLayoutDirection = .leftToRight
126
         userInterfaceLayoutDirection = .leftToRight
119
 
127
 
120
         backButton.translatesAutoresizingMaskIntoConstraints = false
128
         backButton.translatesAutoresizingMaskIntoConstraints = false
121
         backButton.bezelStyle = .rounded
129
         backButton.bezelStyle = .rounded
122
         backButton.isBordered = false
130
         backButton.isBordered = false
123
         backButton.font = .systemFont(ofSize: 13, weight: .semibold)
131
         backButton.font = .systemFont(ofSize: 13, weight: .semibold)
124
-        backButton.contentTintColor = Self.brandBlue
132
+        backButton.contentTintColor = AppDashboardTheme.brandBlue
125
         backButton.target = self
133
         backButton.target = self
126
         backButton.action = #selector(didTapBack)
134
         backButton.action = #selector(didTapBack)
127
 
135
 
128
         titleLabel.font = .systemFont(ofSize: 18, weight: .semibold)
136
         titleLabel.font = .systemFont(ofSize: 18, weight: .semibold)
129
-        titleLabel.textColor = NSColor(srgbRed: 31 / 255, green: 41 / 255, blue: 55 / 255, alpha: 1)
130
 
137
 
131
         exportButton.target = self
138
         exportButton.target = self
132
         exportButton.action = #selector(didTapExportPDF)
139
         exportButton.action = #selector(didTapExportPDF)
133
 
140
 
134
-        let subtitle = NSTextField(wrappingLabelWithString: "Layout matches the CV Maker thumbnail for this template. Export a PDF that matches what you see here (fonts, columns, colours, and rules).")
135
-        subtitle.font = .systemFont(ofSize: 12, weight: .regular)
136
-        subtitle.textColor = Self.secondaryText
137
-        subtitle.maximumNumberOfLines = 0
141
+        subtitleLabel.stringValue = "Layout matches the CV Maker thumbnail for this template. Export a PDF that matches what you see here (fonts, columns, colours, and rules)."
142
+        subtitleLabel.font = .systemFont(ofSize: 12, weight: .regular)
143
+        subtitleLabel.maximumNumberOfLines = 0
138
 
144
 
139
-        let headerCol = NSStackView(views: [backButton, titleLabel, subtitle, exportButton])
145
+        let headerCol = NSStackView(views: [backButton, titleLabel, subtitleLabel, exportButton])
140
         headerCol.orientation = .vertical
146
         headerCol.orientation = .vertical
141
         headerCol.alignment = .leading
147
         headerCol.alignment = .leading
142
         headerCol.spacing = 6
148
         headerCol.spacing = 6
143
         headerCol.setCustomSpacing(14, after: backButton)
149
         headerCol.setCustomSpacing(14, after: backButton)
144
-        headerCol.setCustomSpacing(10, after: subtitle)
150
+        headerCol.setCustomSpacing(10, after: subtitleLabel)
145
         headerCol.translatesAutoresizingMaskIntoConstraints = false
151
         headerCol.translatesAutoresizingMaskIntoConstraints = false
146
 
152
 
147
         contentStack.orientation = .vertical
153
         contentStack.orientation = .vertical
@@ -186,6 +192,21 @@ final class CVFilledPreviewPageView: NSView {
186
             contentStack.topAnchor.constraint(equalTo: documentView.topAnchor, constant: 8),
192
             contentStack.topAnchor.constraint(equalTo: documentView.topAnchor, constant: 8),
187
             contentStack.widthAnchor.constraint(lessThanOrEqualTo: documentView.widthAnchor, constant: -64)
193
             contentStack.widthAnchor.constraint(lessThanOrEqualTo: documentView.widthAnchor, constant: -64)
188
         ])
194
         ])
195
+
196
+        appearanceObserver = NotificationCenter.default.addObserver(
197
+            forName: AppAppearanceManager.didChangeNotification,
198
+            object: nil,
199
+            queue: .main
200
+        ) { [weak self] _ in
201
+            self?.applyCurrentAppearance()
202
+        }
203
+        applyCurrentAppearance()
204
+    }
205
+
206
+    deinit {
207
+        if let appearanceObserver {
208
+            NotificationCenter.default.removeObserver(appearanceObserver)
209
+        }
189
     }
210
     }
190
 
211
 
191
     @available(*, unavailable)
212
     @available(*, unavailable)
@@ -193,6 +214,19 @@ final class CVFilledPreviewPageView: NSView {
193
         fatalError("init(coder:) has not been implemented")
214
         fatalError("init(coder:) has not been implemented")
194
     }
215
     }
195
 
216
 
217
+    override func viewDidChangeEffectiveAppearance() {
218
+        super.viewDidChangeEffectiveAppearance()
219
+        applyCurrentAppearance()
220
+    }
221
+
222
+    func applyCurrentAppearance() {
223
+        layer?.backgroundColor = AppDashboardTheme.pageBackground.cgColor
224
+        backButton.contentTintColor = AppDashboardTheme.brandBlue
225
+        titleLabel.textColor = AppDashboardTheme.primaryText
226
+        subtitleLabel.textColor = AppDashboardTheme.secondaryText
227
+        exportButton.applyCurrentAppearance()
228
+    }
229
+
196
     func configure(profile: SavedProfile, template: CVTemplate) {
230
     func configure(profile: SavedProfile, template: CVTemplate) {
197
         lastProfile = profile
231
         lastProfile = profile
198
         lastTemplate = template
232
         lastTemplate = template

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

@@ -279,6 +279,9 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
279
 
279
 
280
         appearanceModeSegment?.selectedSegment = AppAppearanceManager.shared.mode.segmentIndex
280
         appearanceModeSegment?.selectedSegment = AppAppearanceManager.shared.mode.segmentIndex
281
         cvMakerPageView.applyCurrentAppearance()
281
         cvMakerPageView.applyCurrentAppearance()
282
+        profilesListPageView.applyCurrentAppearance()
283
+        myProfilePageView.applyCurrentAppearance()
284
+        cvFilledPreviewPageView.applyCurrentAppearance()
282
         refreshSettingsPageAppearance(in: settingsPageContainer)
285
         refreshSettingsPageAppearance(in: settingsPageContainer)
283
         rebuildFeatureShortcutCards()
286
         rebuildFeatureShortcutCards()
284
         configureSidebar()
287
         configureSidebar()

+ 126 - 18
App for Indeed/Views/MyProfilePageView.swift

@@ -2,22 +2,71 @@
2
 //  MyProfilePageView.swift
2
 //  MyProfilePageView.swift
3
 //  App for Indeed
3
 //  App for Indeed
4
 //
4
 //
5
-//  Light-theme profile editor: card layout, adaptive two-column rows, and
6
-//  vertical scrolling when the window is short.
5
+//  Profile editor: card layout, adaptive two-column rows, and vertical scrolling
6
+//  when the window is short. Follows the active dashboard light / dark appearance.
7
 //
7
 //
8
 
8
 
9
 import Cocoa
9
 import Cocoa
10
 
10
 
11
 private enum ProfilePagePalette {
11
 private enum ProfilePagePalette {
12
-    static let brandBlue = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 1)
13
-    static let brandBlueHover = NSColor(srgbRed: 28 / 255, green: 70 / 255, blue: 140 / 255, alpha: 1)
14
-    static let pageBackground = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
15
-    static let cardBackground = NSColor(srgbRed: 252 / 255, green: 252 / 255, blue: 252 / 255, alpha: 1)
16
-    static let fieldFill = NSColor(srgbRed: 247 / 255, green: 247 / 255, blue: 247 / 255, alpha: 1)
17
-    static let primaryText = NSColor(srgbRed: 45 / 255, green: 45 / 255, blue: 45 / 255, alpha: 1)
18
-    static let secondaryText = NSColor(srgbRed: 118 / 255, green: 118 / 255, blue: 118 / 255, alpha: 1)
19
-    static let border = NSColor(srgbRed: 212 / 255, green: 210 / 255, blue: 208 / 255, alpha: 1)
20
-    static let destructive = NSColor(srgbRed: 220 / 255, green: 38 / 255, blue: 38 / 255, alpha: 1)
12
+    static var brandBlue: NSColor { AppDashboardTheme.brandBlue }
13
+    static var brandBlueHover: NSColor { AppDashboardTheme.brandBlueHover }
14
+    static var pageBackground: NSColor { AppDashboardTheme.pageBackground }
15
+    static var cardBackground: NSColor { AppDashboardTheme.cardBackground }
16
+    static var fieldFill: NSColor { AppDashboardTheme.profileFieldFill }
17
+    static var primaryText: NSColor { AppDashboardTheme.primaryText }
18
+    static var secondaryText: NSColor { AppDashboardTheme.secondaryText }
19
+    static var border: NSColor { AppDashboardTheme.border }
20
+    static var destructive: NSColor { AppDashboardTheme.profileDestructive }
21
+    static var ctaText: NSColor { AppDashboardTheme.proCTAText }
22
+}
23
+
24
+private enum ProfileThemeAppearance {
25
+    static func refreshFormSubtree(_ root: NSView) {
26
+        for view in root.profileSubviewsRecursive() {
27
+            if let field = view as? NSTextField {
28
+                if field.isEditable {
29
+                    field.textColor = ProfilePagePalette.primaryText
30
+                    if let placeholder = field.placeholderAttributedString?.string, !placeholder.isEmpty {
31
+                        field.placeholderAttributedString = NSAttributedString(
32
+                            string: placeholder,
33
+                            attributes: [
34
+                                .foregroundColor: ProfilePagePalette.secondaryText,
35
+                                .font: field.font ?? NSFont.systemFont(ofSize: 14, weight: .regular),
36
+                                .paragraphStyle: ProfileLayoutEnforcement.leftAlignedParagraphStyle()
37
+                            ]
38
+                        )
39
+                    }
40
+                } else if let font = field.font {
41
+                    if font.pointSize >= 15 {
42
+                        field.textColor = ProfilePagePalette.primaryText
43
+                    } else {
44
+                        field.textColor = ProfilePagePalette.secondaryText
45
+                    }
46
+                }
47
+            }
48
+            guard view.wantsLayer, let layer = view.layer else { continue }
49
+            if layer.cornerRadius == 10, layer.borderWidth == 1 {
50
+                layer.backgroundColor = ProfilePagePalette.fieldFill.cgColor
51
+                layer.borderColor = ProfilePagePalette.border.cgColor
52
+            } else if layer.cornerRadius == 14, layer.borderWidth == 1 {
53
+                layer.backgroundColor = ProfilePagePalette.cardBackground.cgColor
54
+                layer.borderColor = ProfilePagePalette.border.cgColor
55
+            }
56
+        }
57
+    }
58
+}
59
+
60
+private extension NSView {
61
+    func profileSubviewsRecursive() -> [NSView] {
62
+        var result: [NSView] = []
63
+        var stack: [NSView] = [self]
64
+        while let view = stack.popLast() {
65
+            result.append(view)
66
+            stack.append(contentsOf: view.subviews)
67
+        }
68
+        return result
69
+    }
21
 }
70
 }
22
 
71
 
23
 /// Keeps profile text left-aligned and LTR so fields do not collapse to a narrow trailing strip under RTL / natural alignment.
72
 /// Keeps profile text left-aligned and LTR so fields do not collapse to a narrow trailing strip under RTL / natural alignment.
@@ -168,6 +217,7 @@ final class MyProfilePageView: NSView {
168
     private var lastCompactLayout: Bool?
217
     private var lastCompactLayout: Bool?
169
 
218
 
170
     private var referralHelperLabel: NSTextField?
219
     private var referralHelperLabel: NSTextField?
220
+    private var appearanceObserver: NSObjectProtocol?
171
 
221
 
172
     /// Force left-to-right geometry so profile fields span the full width even when the window uses RTL layout.
222
     /// Force left-to-right geometry so profile fields span the full width even when the window uses RTL layout.
173
     override var userInterfaceLayoutDirection: NSUserInterfaceLayoutDirection {
223
     override var userInterfaceLayoutDirection: NSUserInterfaceLayoutDirection {
@@ -178,6 +228,20 @@ final class MyProfilePageView: NSView {
178
     override init(frame frameRect: NSRect) {
228
     override init(frame frameRect: NSRect) {
179
         super.init(frame: frameRect)
229
         super.init(frame: frameRect)
180
         setup()
230
         setup()
231
+        appearanceObserver = NotificationCenter.default.addObserver(
232
+            forName: AppAppearanceManager.didChangeNotification,
233
+            object: nil,
234
+            queue: .main
235
+        ) { [weak self] _ in
236
+            self?.applyCurrentAppearance()
237
+        }
238
+        applyCurrentAppearance()
239
+    }
240
+
241
+    deinit {
242
+        if let appearanceObserver {
243
+            NotificationCenter.default.removeObserver(appearanceObserver)
244
+        }
181
     }
245
     }
182
 
246
 
183
     required init?(coder: NSCoder) {
247
     required init?(coder: NSCoder) {
@@ -185,6 +249,23 @@ final class MyProfilePageView: NSView {
185
         setup()
249
         setup()
186
     }
250
     }
187
 
251
 
252
+    override func viewDidChangeEffectiveAppearance() {
253
+        super.viewDidChangeEffectiveAppearance()
254
+        applyCurrentAppearance()
255
+    }
256
+
257
+    func applyCurrentAppearance() {
258
+        layer?.backgroundColor = ProfilePagePalette.pageBackground.cgColor
259
+        cardView.layer?.backgroundColor = ProfilePagePalette.cardBackground.cgColor
260
+        cardView.layer?.borderColor = ProfilePagePalette.border.cgColor
261
+        backButton.contentTintColor = ProfilePagePalette.brandBlue
262
+        contextLabel.textColor = ProfilePagePalette.primaryText
263
+        ProfileThemeAppearance.refreshFormSubtree(formStack)
264
+        for entry in workExperienceEntries { entry.applyCurrentAppearance() }
265
+        for entry in educationEntries { entry.applyCurrentAppearance() }
266
+        saveButton.applyCurrentAppearance()
267
+    }
268
+
188
     override func viewDidMoveToWindow() {
269
     override func viewDidMoveToWindow() {
189
         super.viewDidMoveToWindow()
270
         super.viewDidMoveToWindow()
190
         guard window != nil else { return }
271
         guard window != nil else { return }
@@ -1224,6 +1305,14 @@ private final class WorkExperienceEntryView: NSView {
1224
         onDelete?()
1305
         onDelete?()
1225
     }
1306
     }
1226
 
1307
 
1308
+    func applyCurrentAppearance() {
1309
+        layer?.backgroundColor = ProfilePagePalette.cardBackground.cgColor
1310
+        layer?.borderColor = ProfilePagePalette.border.cgColor
1311
+        subtitleLabel.textColor = ProfilePagePalette.secondaryText
1312
+        deleteButton.contentTintColor = ProfilePagePalette.destructive
1313
+        ProfileThemeAppearance.refreshFormSubtree(self)
1314
+    }
1315
+
1227
     fileprivate static func labeledFieldStack(title: String, field: NSTextField, placeholder: String) -> NSView {
1316
     fileprivate static func labeledFieldStack(title: String, field: NSTextField, placeholder: String) -> NSView {
1228
         let label = NSTextField(labelWithString: title)
1317
         let label = NSTextField(labelWithString: title)
1229
         label.font = .systemFont(ofSize: 12, weight: .medium)
1318
         label.font = .systemFont(ofSize: 12, weight: .medium)
@@ -1511,6 +1600,14 @@ private final class EducationEntryView: NSView {
1511
     @objc private func didTapDelete() {
1600
     @objc private func didTapDelete() {
1512
         onDelete?()
1601
         onDelete?()
1513
     }
1602
     }
1603
+
1604
+    func applyCurrentAppearance() {
1605
+        layer?.backgroundColor = ProfilePagePalette.cardBackground.cgColor
1606
+        layer?.borderColor = ProfilePagePalette.border.cgColor
1607
+        subtitleLabel.textColor = ProfilePagePalette.secondaryText
1608
+        deleteButton.contentTintColor = ProfilePagePalette.destructive
1609
+        ProfileThemeAppearance.refreshFormSubtree(self)
1610
+    }
1514
 }
1611
 }
1515
 
1612
 
1516
 // MARK: - Primary CTA
1613
 // MARK: - Primary CTA
@@ -1518,6 +1615,7 @@ private final class EducationEntryView: NSView {
1518
 private final class ProfilePrimaryButton: NSButton {
1615
 private final class ProfilePrimaryButton: NSButton {
1519
     private var trackingArea: NSTrackingArea?
1616
     private var trackingArea: NSTrackingArea?
1520
     private var didPushCursor = false
1617
     private var didPushCursor = false
1618
+    private var isHovering = false
1521
 
1619
 
1522
     override init(frame frameRect: NSRect) {
1620
     override init(frame frameRect: NSRect) {
1523
         super.init(frame: frameRect)
1621
         super.init(frame: frameRect)
@@ -1540,13 +1638,18 @@ private final class ProfilePrimaryButton: NSButton {
1540
         bezelStyle = .rounded
1638
         bezelStyle = .rounded
1541
         isBordered = false
1639
         isBordered = false
1542
         font = .systemFont(ofSize: 16, weight: .semibold)
1640
         font = .systemFont(ofSize: 16, weight: .semibold)
1543
-        contentTintColor = .white
1641
+        contentTintColor = ProfilePagePalette.ctaText
1544
         wantsLayer = true
1642
         wantsLayer = true
1545
         layer?.cornerRadius = 14
1643
         layer?.cornerRadius = 14
1546
         if #available(macOS 11.0, *) {
1644
         if #available(macOS 11.0, *) {
1547
             layer?.cornerCurve = .continuous
1645
             layer?.cornerCurve = .continuous
1548
         }
1646
         }
1549
-        layer?.backgroundColor = ProfilePagePalette.brandBlue.cgColor
1647
+        applyCurrentAppearance()
1648
+    }
1649
+
1650
+    func applyCurrentAppearance() {
1651
+        contentTintColor = ProfilePagePalette.ctaText
1652
+        layer?.backgroundColor = (isHovering ? ProfilePagePalette.brandBlueHover : ProfilePagePalette.brandBlue).cgColor
1550
     }
1653
     }
1551
 
1654
 
1552
     override func updateTrackingAreas() {
1655
     override func updateTrackingAreas() {
@@ -1564,7 +1667,8 @@ private final class ProfilePrimaryButton: NSButton {
1564
 
1667
 
1565
     override func mouseEntered(with event: NSEvent) {
1668
     override func mouseEntered(with event: NSEvent) {
1566
         super.mouseEntered(with: event)
1669
         super.mouseEntered(with: event)
1567
-        layer?.backgroundColor = ProfilePagePalette.brandBlueHover.cgColor
1670
+        isHovering = true
1671
+        applyCurrentAppearance()
1568
         if !didPushCursor {
1672
         if !didPushCursor {
1569
             NSCursor.pointingHand.push()
1673
             NSCursor.pointingHand.push()
1570
             didPushCursor = true
1674
             didPushCursor = true
@@ -1573,7 +1677,8 @@ private final class ProfilePrimaryButton: NSButton {
1573
 
1677
 
1574
     override func mouseExited(with event: NSEvent) {
1678
     override func mouseExited(with event: NSEvent) {
1575
         super.mouseExited(with: event)
1679
         super.mouseExited(with: event)
1576
-        layer?.backgroundColor = ProfilePagePalette.brandBlue.cgColor
1680
+        isHovering = false
1681
+        applyCurrentAppearance()
1577
         if didPushCursor {
1682
         if didPushCursor {
1578
             NSCursor.pop()
1683
             NSCursor.pop()
1579
             didPushCursor = false
1684
             didPushCursor = false
@@ -1582,9 +1687,12 @@ private final class ProfilePrimaryButton: NSButton {
1582
 
1687
 
1583
     override func viewWillMove(toWindow newWindow: NSWindow?) {
1688
     override func viewWillMove(toWindow newWindow: NSWindow?) {
1584
         super.viewWillMove(toWindow: newWindow)
1689
         super.viewWillMove(toWindow: newWindow)
1585
-        if newWindow == nil, didPushCursor {
1586
-            NSCursor.pop()
1587
-            didPushCursor = false
1690
+        if newWindow == nil {
1691
+            isHovering = false
1692
+            if didPushCursor {
1693
+                NSCursor.pop()
1694
+                didPushCursor = false
1695
+            }
1588
         }
1696
         }
1589
     }
1697
     }
1590
 }
1698
 }

+ 78 - 28
App for Indeed/Views/ProfilesListPageView.swift

@@ -6,14 +6,15 @@
6
 import Cocoa
6
 import Cocoa
7
 
7
 
8
 private enum ProfilesListPalette {
8
 private enum ProfilesListPalette {
9
-    static let brandBlue = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 1)
10
-    static let brandBlueHover = NSColor(srgbRed: 28 / 255, green: 70 / 255, blue: 140 / 255, alpha: 1)
11
-    static let pageBackground = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
12
-    static let cardBackground = NSColor(srgbRed: 252 / 255, green: 252 / 255, blue: 252 / 255, alpha: 1)
13
-    static let primaryText = NSColor(srgbRed: 45 / 255, green: 45 / 255, blue: 45 / 255, alpha: 1)
14
-    static let secondaryText = NSColor(srgbRed: 118 / 255, green: 118 / 255, blue: 118 / 255, alpha: 1)
15
-    static let border = NSColor(srgbRed: 212 / 255, green: 210 / 255, blue: 208 / 255, alpha: 1)
16
-    static let destructive = NSColor(srgbRed: 220 / 255, green: 38 / 255, blue: 38 / 255, alpha: 1)
9
+    static var brandBlue: NSColor { AppDashboardTheme.brandBlue }
10
+    static var brandBlueHover: NSColor { AppDashboardTheme.brandBlueHover }
11
+    static var pageBackground: NSColor { AppDashboardTheme.pageBackground }
12
+    static var cardBackground: NSColor { AppDashboardTheme.cardBackground }
13
+    static var primaryText: NSColor { AppDashboardTheme.primaryText }
14
+    static var secondaryText: NSColor { AppDashboardTheme.secondaryText }
15
+    static var border: NSColor { AppDashboardTheme.border }
16
+    static var destructive: NSColor { AppDashboardTheme.profileDestructive }
17
+    static var ctaText: NSColor { AppDashboardTheme.proCTAText }
17
 }
18
 }
18
 
19
 
19
 /// Document view for the profiles `NSScrollView`; flipped coordinates keep short content aligned to the top of the clip (avoids a large empty band above the content on macOS).
20
 /// Document view for the profiles `NSScrollView`; flipped coordinates keep short content aligned to the top of the clip (avoids a large empty band above the content on macOS).
@@ -32,9 +33,12 @@ final class ProfilesListPageView: NSView {
32
     private let scrollView = NSScrollView()
33
     private let scrollView = NSScrollView()
33
     private let documentView = ProfilesListDocumentView()
34
     private let documentView = ProfilesListDocumentView()
34
     private let contentStack = NSStackView()
35
     private let contentStack = NSStackView()
36
+    private let titleLabel = NSTextField(labelWithString: "Profiles")
37
+    private let subtitleLabel = NSTextField(wrappingLabelWithString: "")
35
     private let emptyStateLabel = NSTextField(wrappingLabelWithString: "")
38
     private let emptyStateLabel = NSTextField(wrappingLabelWithString: "")
36
     private let addButton = ProfilesPrimaryButton(title: "Add new profile", target: nil, action: nil)
39
     private let addButton = ProfilesPrimaryButton(title: "Add new profile", target: nil, action: nil)
37
     private let pendingFlowLabel = NSTextField(wrappingLabelWithString: "")
40
     private let pendingFlowLabel = NSTextField(wrappingLabelWithString: "")
41
+    private var appearanceObserver: NSObjectProtocol?
38
     /// Non-`nil` after **Use Template & Select Profile** until the dashboard clears the flow (e.g. leaving Profile).
42
     /// Non-`nil` after **Use Template & Select Profile** until the dashboard clears the flow (e.g. leaving Profile).
39
     private var pendingCVTemplateDisplayName: String?
43
     private var pendingCVTemplateDisplayName: String?
40
 
44
 
@@ -46,6 +50,20 @@ final class ProfilesListPageView: NSView {
46
     override init(frame frameRect: NSRect) {
50
     override init(frame frameRect: NSRect) {
47
         super.init(frame: frameRect)
51
         super.init(frame: frameRect)
48
         setup()
52
         setup()
53
+        appearanceObserver = NotificationCenter.default.addObserver(
54
+            forName: AppAppearanceManager.didChangeNotification,
55
+            object: nil,
56
+            queue: .main
57
+        ) { [weak self] _ in
58
+            self?.applyCurrentAppearance()
59
+        }
60
+        applyCurrentAppearance()
61
+    }
62
+
63
+    deinit {
64
+        if let appearanceObserver {
65
+            NotificationCenter.default.removeObserver(appearanceObserver)
66
+        }
49
     }
67
     }
50
 
68
 
51
     required init?(coder: NSCoder) {
69
     required init?(coder: NSCoder) {
@@ -53,6 +71,23 @@ final class ProfilesListPageView: NSView {
53
         setup()
71
         setup()
54
     }
72
     }
55
 
73
 
74
+    override func viewDidChangeEffectiveAppearance() {
75
+        super.viewDidChangeEffectiveAppearance()
76
+        applyCurrentAppearance()
77
+    }
78
+
79
+    func applyCurrentAppearance() {
80
+        layer?.backgroundColor = ProfilesListPalette.pageBackground.cgColor
81
+        titleLabel.textColor = ProfilesListPalette.primaryText
82
+        subtitleLabel.textColor = ProfilesListPalette.secondaryText
83
+        emptyStateLabel.textColor = ProfilesListPalette.secondaryText
84
+        pendingFlowLabel.textColor = ProfilesListPalette.brandBlue
85
+        addButton.applyCurrentAppearance()
86
+        for case let row as ProfileListRowView in contentStack.arrangedSubviews {
87
+            row.applyCurrentAppearance()
88
+        }
89
+    }
90
+
56
     func reloadFromStore() {
91
     func reloadFromStore() {
57
         for row in contentStack.arrangedSubviews {
92
         for row in contentStack.arrangedSubviews {
58
             contentStack.removeArrangedSubview(row)
93
             contentStack.removeArrangedSubview(row)
@@ -91,14 +126,11 @@ final class ProfilesListPageView: NSView {
91
         layer?.backgroundColor = ProfilesListPalette.pageBackground.cgColor
126
         layer?.backgroundColor = ProfilesListPalette.pageBackground.cgColor
92
         userInterfaceLayoutDirection = .leftToRight
127
         userInterfaceLayoutDirection = .leftToRight
93
 
128
 
94
-        let title = NSTextField(labelWithString: "Profiles")
95
-        title.font = .systemFont(ofSize: 22, weight: .semibold)
96
-        title.textColor = ProfilesListPalette.primaryText
129
+        titleLabel.font = .systemFont(ofSize: 22, weight: .semibold)
97
 
130
 
98
-        let subtitle = NSTextField(wrappingLabelWithString: "Create and manage CV profiles. Each profile stores your details on this Mac.")
99
-        subtitle.font = .systemFont(ofSize: 13, weight: .regular)
100
-        subtitle.textColor = ProfilesListPalette.secondaryText
101
-        subtitle.maximumNumberOfLines = 0
131
+        subtitleLabel.stringValue = "Create and manage CV profiles. Each profile stores your details on this Mac."
132
+        subtitleLabel.font = .systemFont(ofSize: 13, weight: .regular)
133
+        subtitleLabel.maximumNumberOfLines = 0
102
 
134
 
103
         emptyStateLabel.stringValue = "No profiles yet. Tap “Add new profile” to create your first one."
135
         emptyStateLabel.stringValue = "No profiles yet. Tap “Add new profile” to create your first one."
104
         emptyStateLabel.font = .systemFont(ofSize: 13, weight: .regular)
136
         emptyStateLabel.font = .systemFont(ofSize: 13, weight: .regular)
@@ -115,7 +147,7 @@ final class ProfilesListPageView: NSView {
115
             addButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 76)
147
             addButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 76)
116
         ])
148
         ])
117
 
149
 
118
-        let titleSubtitleStack = NSStackView(views: [title, subtitle])
150
+        let titleSubtitleStack = NSStackView(views: [titleLabel, subtitleLabel])
119
         titleSubtitleStack.orientation = .vertical
151
         titleSubtitleStack.orientation = .vertical
120
         titleSubtitleStack.alignment = .leading
152
         titleSubtitleStack.alignment = .leading
121
         titleSubtitleStack.spacing = 10
153
         titleSubtitleStack.spacing = 10
@@ -203,6 +235,8 @@ private final class ProfileListRowView: NSView {
203
     var onBuildCV: ((UUID) -> Void)?
235
     var onBuildCV: ((UUID) -> Void)?
204
 
236
 
205
     private let profileID: UUID
237
     private let profileID: UUID
238
+    private let nameLabel = NSTextField(labelWithString: "")
239
+    private let detailLabel = NSTextField(wrappingLabelWithString: "")
206
     private let buildCVButton = NSButton(title: "Build CV", target: nil, action: nil)
240
     private let buildCVButton = NSButton(title: "Build CV", target: nil, action: nil)
207
     private let editButton = NSButton(title: "Edit", target: nil, action: nil)
241
     private let editButton = NSButton(title: "Edit", target: nil, action: nil)
208
     private let deleteButton = NSButton(title: "Delete", target: nil, action: nil)
242
     private let deleteButton = NSButton(title: "Delete", target: nil, action: nil)
@@ -220,17 +254,15 @@ private final class ProfileListRowView: NSView {
220
             layer?.cornerCurve = .continuous
254
             layer?.cornerCurve = .continuous
221
         }
255
         }
222
 
256
 
223
-        let name = NSTextField(labelWithString: profile.profileDisplayName.isEmpty ? "Untitled profile" : profile.profileDisplayName)
224
-        name.font = .systemFont(ofSize: 15, weight: .semibold)
225
-        name.textColor = ProfilesListPalette.primaryText
257
+        nameLabel.stringValue = profile.profileDisplayName.isEmpty ? "Untitled profile" : profile.profileDisplayName
258
+        nameLabel.font = .systemFont(ofSize: 15, weight: .semibold)
226
 
259
 
227
         let detailParts = [profile.personal.fullName, profile.personal.email].filter { !$0.isEmpty }
260
         let detailParts = [profile.personal.fullName, profile.personal.email].filter { !$0.isEmpty }
228
-        let detail = NSTextField(wrappingLabelWithString: detailParts.isEmpty ? "No contact details yet" : detailParts.joined(separator: " · "))
229
-        detail.font = .systemFont(ofSize: 12, weight: .regular)
230
-        detail.textColor = ProfilesListPalette.secondaryText
231
-        detail.maximumNumberOfLines = 2
261
+        detailLabel.stringValue = detailParts.isEmpty ? "No contact details yet" : detailParts.joined(separator: " · ")
262
+        detailLabel.font = .systemFont(ofSize: 12, weight: .regular)
263
+        detailLabel.maximumNumberOfLines = 2
232
 
264
 
233
-        let textStack = NSStackView(views: [name, detail])
265
+        let textStack = NSStackView(views: [nameLabel, detailLabel])
234
         textStack.orientation = .vertical
266
         textStack.orientation = .vertical
235
         textStack.alignment = .leading
267
         textStack.alignment = .leading
236
         textStack.spacing = 4
268
         textStack.spacing = 4
@@ -300,6 +332,15 @@ private final class ProfileListRowView: NSView {
300
     @objc private func didTapDelete() {
332
     @objc private func didTapDelete() {
301
         onDelete?(profileID)
333
         onDelete?(profileID)
302
     }
334
     }
335
+
336
+    func applyCurrentAppearance() {
337
+        layer?.borderColor = ProfilesListPalette.border.cgColor
338
+        layer?.backgroundColor = ProfilesListPalette.cardBackground.cgColor
339
+        nameLabel.textColor = ProfilesListPalette.primaryText
340
+        detailLabel.textColor = ProfilesListPalette.secondaryText
341
+        buildCVButton.contentTintColor = ProfilesListPalette.brandBlue
342
+        deleteButton.contentTintColor = ProfilesListPalette.destructive
343
+    }
303
 }
344
 }
304
 
345
 
305
 // MARK: - Primary CTA (matches job cards’ Apply: 13pt semibold, 32pt tall, 8pt corners)
346
 // MARK: - Primary CTA (matches job cards’ Apply: 13pt semibold, 32pt tall, 8pt corners)
@@ -332,13 +373,20 @@ private final class ProfilesPrimaryButton: NSButton {
332
         bezelStyle = .rounded
373
         bezelStyle = .rounded
333
         isBordered = false
374
         isBordered = false
334
         font = .systemFont(ofSize: 13, weight: .semibold)
375
         font = .systemFont(ofSize: 13, weight: .semibold)
335
-        contentTintColor = .white
376
+        contentTintColor = ProfilesListPalette.ctaText
336
         focusRingType = .none
377
         focusRingType = .none
337
         wantsLayer = true
378
         wantsLayer = true
338
         layer?.cornerRadius = 8
379
         layer?.cornerRadius = 8
339
-        layer?.backgroundColor = ProfilesListPalette.brandBlue.cgColor
380
+        applyCurrentAppearance()
340
     }
381
     }
341
 
382
 
383
+    func applyCurrentAppearance() {
384
+        contentTintColor = ProfilesListPalette.ctaText
385
+        layer?.backgroundColor = (isHovering ? ProfilesListPalette.brandBlueHover : ProfilesListPalette.brandBlue).cgColor
386
+    }
387
+
388
+    private var isHovering = false
389
+
342
     override var intrinsicContentSize: NSSize {
390
     override var intrinsicContentSize: NSSize {
343
         let base = super.intrinsicContentSize
391
         let base = super.intrinsicContentSize
344
         guard base.width != NSView.noIntrinsicMetric, base.width >= 1 else { return base }
392
         guard base.width != NSView.noIntrinsicMetric, base.width >= 1 else { return base }
@@ -360,7 +408,8 @@ private final class ProfilesPrimaryButton: NSButton {
360
 
408
 
361
     override func mouseEntered(with event: NSEvent) {
409
     override func mouseEntered(with event: NSEvent) {
362
         super.mouseEntered(with: event)
410
         super.mouseEntered(with: event)
363
-        layer?.backgroundColor = ProfilesListPalette.brandBlueHover.cgColor
411
+        isHovering = true
412
+        applyCurrentAppearance()
364
         if !didPushCursor {
413
         if !didPushCursor {
365
             NSCursor.pointingHand.push()
414
             NSCursor.pointingHand.push()
366
             didPushCursor = true
415
             didPushCursor = true
@@ -369,7 +418,8 @@ private final class ProfilesPrimaryButton: NSButton {
369
 
418
 
370
     override func mouseExited(with event: NSEvent) {
419
     override func mouseExited(with event: NSEvent) {
371
         super.mouseExited(with: event)
420
         super.mouseExited(with: event)
372
-        layer?.backgroundColor = ProfilesListPalette.brandBlue.cgColor
421
+        isHovering = false
422
+        applyCurrentAppearance()
373
         if didPushCursor {
423
         if didPushCursor {
374
             NSCursor.pop()
424
             NSCursor.pop()
375
             didPushCursor = false
425
             didPushCursor = false