|
|
@@ -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
|