|
|
@@ -6,14 +6,15 @@
|
|
|
import Cocoa
|
|
|
|
|
|
private enum ProfilesListPalette {
|
|
|
- static let brandBlue = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 1)
|
|
|
- static let brandBlueHover = NSColor(srgbRed: 28 / 255, green: 70 / 255, blue: 140 / 255, alpha: 1)
|
|
|
- static let pageBackground = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
|
|
|
- static let cardBackground = NSColor(srgbRed: 252 / 255, green: 252 / 255, blue: 252 / 255, alpha: 1)
|
|
|
- static let primaryText = NSColor(srgbRed: 45 / 255, green: 45 / 255, blue: 45 / 255, alpha: 1)
|
|
|
- static let secondaryText = NSColor(srgbRed: 118 / 255, green: 118 / 255, blue: 118 / 255, alpha: 1)
|
|
|
- static let border = NSColor(srgbRed: 212 / 255, green: 210 / 255, blue: 208 / 255, alpha: 1)
|
|
|
- static let destructive = NSColor(srgbRed: 220 / 255, green: 38 / 255, blue: 38 / 255, alpha: 1)
|
|
|
+ static var brandBlue: NSColor { AppDashboardTheme.brandBlue }
|
|
|
+ static var brandBlueHover: NSColor { AppDashboardTheme.brandBlueHover }
|
|
|
+ static var pageBackground: NSColor { AppDashboardTheme.pageBackground }
|
|
|
+ static var cardBackground: NSColor { AppDashboardTheme.cardBackground }
|
|
|
+ static var primaryText: NSColor { AppDashboardTheme.primaryText }
|
|
|
+ static var secondaryText: NSColor { AppDashboardTheme.secondaryText }
|
|
|
+ static var border: NSColor { AppDashboardTheme.border }
|
|
|
+ static var destructive: NSColor { AppDashboardTheme.profileDestructive }
|
|
|
+ static var ctaText: NSColor { AppDashboardTheme.proCTAText }
|
|
|
}
|
|
|
|
|
|
/// 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 {
|
|
|
private let scrollView = NSScrollView()
|
|
|
private let documentView = ProfilesListDocumentView()
|
|
|
private let contentStack = NSStackView()
|
|
|
+ private let titleLabel = NSTextField(labelWithString: "Profiles")
|
|
|
+ private let subtitleLabel = NSTextField(wrappingLabelWithString: "")
|
|
|
private let emptyStateLabel = NSTextField(wrappingLabelWithString: "")
|
|
|
private let addButton = ProfilesPrimaryButton(title: "Add new profile", target: nil, action: nil)
|
|
|
private let pendingFlowLabel = NSTextField(wrappingLabelWithString: "")
|
|
|
+ private var appearanceObserver: NSObjectProtocol?
|
|
|
/// Non-`nil` after **Use Template & Select Profile** until the dashboard clears the flow (e.g. leaving Profile).
|
|
|
private var pendingCVTemplateDisplayName: String?
|
|
|
|
|
|
@@ -46,6 +50,20 @@ final class ProfilesListPageView: NSView {
|
|
|
override init(frame frameRect: NSRect) {
|
|
|
super.init(frame: frameRect)
|
|
|
setup()
|
|
|
+ appearanceObserver = NotificationCenter.default.addObserver(
|
|
|
+ forName: AppAppearanceManager.didChangeNotification,
|
|
|
+ object: nil,
|
|
|
+ queue: .main
|
|
|
+ ) { [weak self] _ in
|
|
|
+ self?.applyCurrentAppearance()
|
|
|
+ }
|
|
|
+ applyCurrentAppearance()
|
|
|
+ }
|
|
|
+
|
|
|
+ deinit {
|
|
|
+ if let appearanceObserver {
|
|
|
+ NotificationCenter.default.removeObserver(appearanceObserver)
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
required init?(coder: NSCoder) {
|
|
|
@@ -53,6 +71,23 @@ final class ProfilesListPageView: NSView {
|
|
|
setup()
|
|
|
}
|
|
|
|
|
|
+ override func viewDidChangeEffectiveAppearance() {
|
|
|
+ super.viewDidChangeEffectiveAppearance()
|
|
|
+ applyCurrentAppearance()
|
|
|
+ }
|
|
|
+
|
|
|
+ func applyCurrentAppearance() {
|
|
|
+ layer?.backgroundColor = ProfilesListPalette.pageBackground.cgColor
|
|
|
+ titleLabel.textColor = ProfilesListPalette.primaryText
|
|
|
+ subtitleLabel.textColor = ProfilesListPalette.secondaryText
|
|
|
+ emptyStateLabel.textColor = ProfilesListPalette.secondaryText
|
|
|
+ pendingFlowLabel.textColor = ProfilesListPalette.brandBlue
|
|
|
+ addButton.applyCurrentAppearance()
|
|
|
+ for case let row as ProfileListRowView in contentStack.arrangedSubviews {
|
|
|
+ row.applyCurrentAppearance()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
func reloadFromStore() {
|
|
|
for row in contentStack.arrangedSubviews {
|
|
|
contentStack.removeArrangedSubview(row)
|
|
|
@@ -91,14 +126,11 @@ final class ProfilesListPageView: NSView {
|
|
|
layer?.backgroundColor = ProfilesListPalette.pageBackground.cgColor
|
|
|
userInterfaceLayoutDirection = .leftToRight
|
|
|
|
|
|
- let title = NSTextField(labelWithString: "Profiles")
|
|
|
- title.font = .systemFont(ofSize: 22, weight: .semibold)
|
|
|
- title.textColor = ProfilesListPalette.primaryText
|
|
|
+ titleLabel.font = .systemFont(ofSize: 22, weight: .semibold)
|
|
|
|
|
|
- let subtitle = NSTextField(wrappingLabelWithString: "Create and manage CV profiles. Each profile stores your details on this Mac.")
|
|
|
- subtitle.font = .systemFont(ofSize: 13, weight: .regular)
|
|
|
- subtitle.textColor = ProfilesListPalette.secondaryText
|
|
|
- subtitle.maximumNumberOfLines = 0
|
|
|
+ subtitleLabel.stringValue = "Create and manage CV profiles. Each profile stores your details on this Mac."
|
|
|
+ subtitleLabel.font = .systemFont(ofSize: 13, weight: .regular)
|
|
|
+ subtitleLabel.maximumNumberOfLines = 0
|
|
|
|
|
|
emptyStateLabel.stringValue = "No profiles yet. Tap “Add new profile” to create your first one."
|
|
|
emptyStateLabel.font = .systemFont(ofSize: 13, weight: .regular)
|
|
|
@@ -115,7 +147,7 @@ final class ProfilesListPageView: NSView {
|
|
|
addButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 76)
|
|
|
])
|
|
|
|
|
|
- let titleSubtitleStack = NSStackView(views: [title, subtitle])
|
|
|
+ let titleSubtitleStack = NSStackView(views: [titleLabel, subtitleLabel])
|
|
|
titleSubtitleStack.orientation = .vertical
|
|
|
titleSubtitleStack.alignment = .leading
|
|
|
titleSubtitleStack.spacing = 10
|
|
|
@@ -203,6 +235,8 @@ private final class ProfileListRowView: NSView {
|
|
|
var onBuildCV: ((UUID) -> Void)?
|
|
|
|
|
|
private let profileID: UUID
|
|
|
+ private let nameLabel = NSTextField(labelWithString: "")
|
|
|
+ private let detailLabel = NSTextField(wrappingLabelWithString: "")
|
|
|
private let buildCVButton = NSButton(title: "Build CV", target: nil, action: nil)
|
|
|
private let editButton = NSButton(title: "Edit", target: nil, action: nil)
|
|
|
private let deleteButton = NSButton(title: "Delete", target: nil, action: nil)
|
|
|
@@ -220,17 +254,15 @@ private final class ProfileListRowView: NSView {
|
|
|
layer?.cornerCurve = .continuous
|
|
|
}
|
|
|
|
|
|
- let name = NSTextField(labelWithString: profile.profileDisplayName.isEmpty ? "Untitled profile" : profile.profileDisplayName)
|
|
|
- name.font = .systemFont(ofSize: 15, weight: .semibold)
|
|
|
- name.textColor = ProfilesListPalette.primaryText
|
|
|
+ nameLabel.stringValue = profile.profileDisplayName.isEmpty ? "Untitled profile" : profile.profileDisplayName
|
|
|
+ nameLabel.font = .systemFont(ofSize: 15, weight: .semibold)
|
|
|
|
|
|
let detailParts = [profile.personal.fullName, profile.personal.email].filter { !$0.isEmpty }
|
|
|
- let detail = NSTextField(wrappingLabelWithString: detailParts.isEmpty ? "No contact details yet" : detailParts.joined(separator: " · "))
|
|
|
- detail.font = .systemFont(ofSize: 12, weight: .regular)
|
|
|
- detail.textColor = ProfilesListPalette.secondaryText
|
|
|
- detail.maximumNumberOfLines = 2
|
|
|
+ detailLabel.stringValue = detailParts.isEmpty ? "No contact details yet" : detailParts.joined(separator: " · ")
|
|
|
+ detailLabel.font = .systemFont(ofSize: 12, weight: .regular)
|
|
|
+ detailLabel.maximumNumberOfLines = 2
|
|
|
|
|
|
- let textStack = NSStackView(views: [name, detail])
|
|
|
+ let textStack = NSStackView(views: [nameLabel, detailLabel])
|
|
|
textStack.orientation = .vertical
|
|
|
textStack.alignment = .leading
|
|
|
textStack.spacing = 4
|
|
|
@@ -300,6 +332,15 @@ private final class ProfileListRowView: NSView {
|
|
|
@objc private func didTapDelete() {
|
|
|
onDelete?(profileID)
|
|
|
}
|
|
|
+
|
|
|
+ func applyCurrentAppearance() {
|
|
|
+ layer?.borderColor = ProfilesListPalette.border.cgColor
|
|
|
+ layer?.backgroundColor = ProfilesListPalette.cardBackground.cgColor
|
|
|
+ nameLabel.textColor = ProfilesListPalette.primaryText
|
|
|
+ detailLabel.textColor = ProfilesListPalette.secondaryText
|
|
|
+ buildCVButton.contentTintColor = ProfilesListPalette.brandBlue
|
|
|
+ deleteButton.contentTintColor = ProfilesListPalette.destructive
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
// MARK: - Primary CTA (matches job cards’ Apply: 13pt semibold, 32pt tall, 8pt corners)
|
|
|
@@ -332,13 +373,20 @@ private final class ProfilesPrimaryButton: NSButton {
|
|
|
bezelStyle = .rounded
|
|
|
isBordered = false
|
|
|
font = .systemFont(ofSize: 13, weight: .semibold)
|
|
|
- contentTintColor = .white
|
|
|
+ contentTintColor = ProfilesListPalette.ctaText
|
|
|
focusRingType = .none
|
|
|
wantsLayer = true
|
|
|
layer?.cornerRadius = 8
|
|
|
- layer?.backgroundColor = ProfilesListPalette.brandBlue.cgColor
|
|
|
+ applyCurrentAppearance()
|
|
|
}
|
|
|
|
|
|
+ func applyCurrentAppearance() {
|
|
|
+ contentTintColor = ProfilesListPalette.ctaText
|
|
|
+ layer?.backgroundColor = (isHovering ? ProfilesListPalette.brandBlueHover : ProfilesListPalette.brandBlue).cgColor
|
|
|
+ }
|
|
|
+
|
|
|
+ private var isHovering = false
|
|
|
+
|
|
|
override var intrinsicContentSize: NSSize {
|
|
|
let base = super.intrinsicContentSize
|
|
|
guard base.width != NSView.noIntrinsicMetric, base.width >= 1 else { return base }
|
|
|
@@ -360,7 +408,8 @@ private final class ProfilesPrimaryButton: NSButton {
|
|
|
|
|
|
override func mouseEntered(with event: NSEvent) {
|
|
|
super.mouseEntered(with: event)
|
|
|
- layer?.backgroundColor = ProfilesListPalette.brandBlueHover.cgColor
|
|
|
+ isHovering = true
|
|
|
+ applyCurrentAppearance()
|
|
|
if !didPushCursor {
|
|
|
NSCursor.pointingHand.push()
|
|
|
didPushCursor = true
|
|
|
@@ -369,7 +418,8 @@ private final class ProfilesPrimaryButton: NSButton {
|
|
|
|
|
|
override func mouseExited(with event: NSEvent) {
|
|
|
super.mouseExited(with: event)
|
|
|
- layer?.backgroundColor = ProfilesListPalette.brandBlue.cgColor
|
|
|
+ isHovering = false
|
|
|
+ applyCurrentAppearance()
|
|
|
if didPushCursor {
|
|
|
NSCursor.pop()
|
|
|
didPushCursor = false
|