Bladeren bron

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 weken geleden
bovenliggende
commit
fdb581a4ba

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

@@ -287,4 +287,16 @@ enum AppDashboardTheme {
287 287
             ? NSColor(srgbRed: 58 / 255, green: 58 / 255, blue: 60 / 255, alpha: 1)
288 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 15
 /// Same metrics as job cards’ **Apply** (`DashboardView` `JobPayloadButton`): 13pt semibold, 32pt tall, 8pt corners.
16 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 20
     /// Slightly wider than default title metrics so the label is not flush to the pill edges.
20 21
     private static let horizontalOutset: CGFloat = 20
21 22
 
@@ -34,7 +35,7 @@ private final class CVPreviewPrimaryCTAButton: NSButton {
34 35
         wantsLayer = true
35 36
         layer?.cornerRadius = 8
36 37
         layer?.backgroundColor = Self.fill.cgColor
37
-        contentTintColor = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
38
+        contentTintColor = Self.labelColor
38 39
         focusRingType = .none
39 40
         setContentHuggingPriority(.required, for: .horizontal)
40 41
         setContentCompressionResistancePriority(.required, for: .horizontal)
@@ -64,9 +65,17 @@ private final class CVPreviewPrimaryCTAButton: NSButton {
64 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 75
     override func mouseEntered(with event: NSEvent) {
68 76
         super.mouseEntered(with: event)
69
-        layer?.backgroundColor = Self.fillHover.cgColor
77
+        isHovering = true
78
+        applyCurrentAppearance()
70 79
         if !didPushCursor {
71 80
             NSCursor.pointingHand.push()
72 81
             didPushCursor = true
@@ -75,7 +84,8 @@ private final class CVPreviewPrimaryCTAButton: NSButton {
75 84
 
76 85
     override func mouseExited(with event: NSEvent) {
77 86
         super.mouseExited(with: event)
78
-        layer?.backgroundColor = Self.fill.cgColor
87
+        isHovering = false
88
+        applyCurrentAppearance()
79 89
         if didPushCursor {
80 90
             NSCursor.pop()
81 91
             didPushCursor = false
@@ -107,41 +117,37 @@ final class CVFilledPreviewPageView: NSView {
107 117
     private var lastProfile: SavedProfile?
108 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 123
     override init(frame frameRect: NSRect) {
115 124
         super.init(frame: frameRect)
116 125
         wantsLayer = true
117
-        layer?.backgroundColor = Self.pageBackground.cgColor
118 126
         userInterfaceLayoutDirection = .leftToRight
119 127
 
120 128
         backButton.translatesAutoresizingMaskIntoConstraints = false
121 129
         backButton.bezelStyle = .rounded
122 130
         backButton.isBordered = false
123 131
         backButton.font = .systemFont(ofSize: 13, weight: .semibold)
124
-        backButton.contentTintColor = Self.brandBlue
132
+        backButton.contentTintColor = AppDashboardTheme.brandBlue
125 133
         backButton.target = self
126 134
         backButton.action = #selector(didTapBack)
127 135
 
128 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 138
         exportButton.target = self
132 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 146
         headerCol.orientation = .vertical
141 147
         headerCol.alignment = .leading
142 148
         headerCol.spacing = 6
143 149
         headerCol.setCustomSpacing(14, after: backButton)
144
-        headerCol.setCustomSpacing(10, after: subtitle)
150
+        headerCol.setCustomSpacing(10, after: subtitleLabel)
145 151
         headerCol.translatesAutoresizingMaskIntoConstraints = false
146 152
 
147 153
         contentStack.orientation = .vertical
@@ -186,6 +192,21 @@ final class CVFilledPreviewPageView: NSView {
186 192
             contentStack.topAnchor.constraint(equalTo: documentView.topAnchor, constant: 8),
187 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 212
     @available(*, unavailable)
@@ -193,6 +214,19 @@ final class CVFilledPreviewPageView: NSView {
193 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 230
     func configure(profile: SavedProfile, template: CVTemplate) {
197 231
         lastProfile = profile
198 232
         lastTemplate = template

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

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

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

@@ -2,22 +2,71 @@
2 2
 //  MyProfilePageView.swift
3 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 9
 import Cocoa
10 10
 
11 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 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 217
     private var lastCompactLayout: Bool?
169 218
 
170 219
     private var referralHelperLabel: NSTextField?
220
+    private var appearanceObserver: NSObjectProtocol?
171 221
 
172 222
     /// Force left-to-right geometry so profile fields span the full width even when the window uses RTL layout.
173 223
     override var userInterfaceLayoutDirection: NSUserInterfaceLayoutDirection {
@@ -178,6 +228,20 @@ final class MyProfilePageView: NSView {
178 228
     override init(frame frameRect: NSRect) {
179 229
         super.init(frame: frameRect)
180 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 247
     required init?(coder: NSCoder) {
@@ -185,6 +249,23 @@ final class MyProfilePageView: NSView {
185 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 269
     override func viewDidMoveToWindow() {
189 270
         super.viewDidMoveToWindow()
190 271
         guard window != nil else { return }
@@ -1224,6 +1305,14 @@ private final class WorkExperienceEntryView: NSView {
1224 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 1316
     fileprivate static func labeledFieldStack(title: String, field: NSTextField, placeholder: String) -> NSView {
1228 1317
         let label = NSTextField(labelWithString: title)
1229 1318
         label.font = .systemFont(ofSize: 12, weight: .medium)
@@ -1511,6 +1600,14 @@ private final class EducationEntryView: NSView {
1511 1600
     @objc private func didTapDelete() {
1512 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 1613
 // MARK: - Primary CTA
@@ -1518,6 +1615,7 @@ private final class EducationEntryView: NSView {
1518 1615
 private final class ProfilePrimaryButton: NSButton {
1519 1616
     private var trackingArea: NSTrackingArea?
1520 1617
     private var didPushCursor = false
1618
+    private var isHovering = false
1521 1619
 
1522 1620
     override init(frame frameRect: NSRect) {
1523 1621
         super.init(frame: frameRect)
@@ -1540,13 +1638,18 @@ private final class ProfilePrimaryButton: NSButton {
1540 1638
         bezelStyle = .rounded
1541 1639
         isBordered = false
1542 1640
         font = .systemFont(ofSize: 16, weight: .semibold)
1543
-        contentTintColor = .white
1641
+        contentTintColor = ProfilePagePalette.ctaText
1544 1642
         wantsLayer = true
1545 1643
         layer?.cornerRadius = 14
1546 1644
         if #available(macOS 11.0, *) {
1547 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 1655
     override func updateTrackingAreas() {
@@ -1564,7 +1667,8 @@ private final class ProfilePrimaryButton: NSButton {
1564 1667
 
1565 1668
     override func mouseEntered(with event: NSEvent) {
1566 1669
         super.mouseEntered(with: event)
1567
-        layer?.backgroundColor = ProfilePagePalette.brandBlueHover.cgColor
1670
+        isHovering = true
1671
+        applyCurrentAppearance()
1568 1672
         if !didPushCursor {
1569 1673
             NSCursor.pointingHand.push()
1570 1674
             didPushCursor = true
@@ -1573,7 +1677,8 @@ private final class ProfilePrimaryButton: NSButton {
1573 1677
 
1574 1678
     override func mouseExited(with event: NSEvent) {
1575 1679
         super.mouseExited(with: event)
1576
-        layer?.backgroundColor = ProfilePagePalette.brandBlue.cgColor
1680
+        isHovering = false
1681
+        applyCurrentAppearance()
1577 1682
         if didPushCursor {
1578 1683
             NSCursor.pop()
1579 1684
             didPushCursor = false
@@ -1582,9 +1687,12 @@ private final class ProfilePrimaryButton: NSButton {
1582 1687
 
1583 1688
     override func viewWillMove(toWindow newWindow: NSWindow?) {
1584 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 6
 import Cocoa
7 7
 
8 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 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 33
     private let scrollView = NSScrollView()
33 34
     private let documentView = ProfilesListDocumentView()
34 35
     private let contentStack = NSStackView()
36
+    private let titleLabel = NSTextField(labelWithString: "Profiles")
37
+    private let subtitleLabel = NSTextField(wrappingLabelWithString: "")
35 38
     private let emptyStateLabel = NSTextField(wrappingLabelWithString: "")
36 39
     private let addButton = ProfilesPrimaryButton(title: "Add new profile", target: nil, action: nil)
37 40
     private let pendingFlowLabel = NSTextField(wrappingLabelWithString: "")
41
+    private var appearanceObserver: NSObjectProtocol?
38 42
     /// Non-`nil` after **Use Template & Select Profile** until the dashboard clears the flow (e.g. leaving Profile).
39 43
     private var pendingCVTemplateDisplayName: String?
40 44
 
@@ -46,6 +50,20 @@ final class ProfilesListPageView: NSView {
46 50
     override init(frame frameRect: NSRect) {
47 51
         super.init(frame: frameRect)
48 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 69
     required init?(coder: NSCoder) {
@@ -53,6 +71,23 @@ final class ProfilesListPageView: NSView {
53 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 91
     func reloadFromStore() {
57 92
         for row in contentStack.arrangedSubviews {
58 93
             contentStack.removeArrangedSubview(row)
@@ -91,14 +126,11 @@ final class ProfilesListPageView: NSView {
91 126
         layer?.backgroundColor = ProfilesListPalette.pageBackground.cgColor
92 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 135
         emptyStateLabel.stringValue = "No profiles yet. Tap “Add new profile” to create your first one."
104 136
         emptyStateLabel.font = .systemFont(ofSize: 13, weight: .regular)
@@ -115,7 +147,7 @@ final class ProfilesListPageView: NSView {
115 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 151
         titleSubtitleStack.orientation = .vertical
120 152
         titleSubtitleStack.alignment = .leading
121 153
         titleSubtitleStack.spacing = 10
@@ -203,6 +235,8 @@ private final class ProfileListRowView: NSView {
203 235
     var onBuildCV: ((UUID) -> Void)?
204 236
 
205 237
     private let profileID: UUID
238
+    private let nameLabel = NSTextField(labelWithString: "")
239
+    private let detailLabel = NSTextField(wrappingLabelWithString: "")
206 240
     private let buildCVButton = NSButton(title: "Build CV", target: nil, action: nil)
207 241
     private let editButton = NSButton(title: "Edit", target: nil, action: nil)
208 242
     private let deleteButton = NSButton(title: "Delete", target: nil, action: nil)
@@ -220,17 +254,15 @@ private final class ProfileListRowView: NSView {
220 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 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 266
         textStack.orientation = .vertical
235 267
         textStack.alignment = .leading
236 268
         textStack.spacing = 4
@@ -300,6 +332,15 @@ private final class ProfileListRowView: NSView {
300 332
     @objc private func didTapDelete() {
301 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 346
 // MARK: - Primary CTA (matches job cards’ Apply: 13pt semibold, 32pt tall, 8pt corners)
@@ -332,13 +373,20 @@ private final class ProfilesPrimaryButton: NSButton {
332 373
         bezelStyle = .rounded
333 374
         isBordered = false
334 375
         font = .systemFont(ofSize: 13, weight: .semibold)
335
-        contentTintColor = .white
376
+        contentTintColor = ProfilesListPalette.ctaText
336 377
         focusRingType = .none
337 378
         wantsLayer = true
338 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 390
     override var intrinsicContentSize: NSSize {
343 391
         let base = super.intrinsicContentSize
344 392
         guard base.width != NSView.noIntrinsicMetric, base.width >= 1 else { return base }
@@ -360,7 +408,8 @@ private final class ProfilesPrimaryButton: NSButton {
360 408
 
361 409
     override func mouseEntered(with event: NSEvent) {
362 410
         super.mouseEntered(with: event)
363
-        layer?.backgroundColor = ProfilesListPalette.brandBlueHover.cgColor
411
+        isHovering = true
412
+        applyCurrentAppearance()
364 413
         if !didPushCursor {
365 414
             NSCursor.pointingHand.push()
366 415
             didPushCursor = true
@@ -369,7 +418,8 @@ private final class ProfilesPrimaryButton: NSButton {
369 418
 
370 419
     override func mouseExited(with event: NSEvent) {
371 420
         super.mouseExited(with: event)
372
-        layer?.backgroundColor = ProfilesListPalette.brandBlue.cgColor
421
+        isHovering = false
422
+        applyCurrentAppearance()
373 423
         if didPushCursor {
374 424
             NSCursor.pop()
375 425
             didPushCursor = false