|
|
@@ -12,6 +12,85 @@ private final class CVPreviewFlippedDocumentView: NSView {
|
|
|
override var isFlipped: Bool { true }
|
|
|
}
|
|
|
|
|
|
+/// Same metrics as job cards’ **Apply** (`DashboardView` `JobPayloadButton`): 13pt semibold, 32pt tall, 8pt corners.
|
|
|
+private final class CVPreviewPrimaryCTAButton: NSButton {
|
|
|
+ private static let fill = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 1)
|
|
|
+ private static let fillHover = NSColor(srgbRed: 28 / 255, green: 70 / 255, blue: 140 / 255, alpha: 1)
|
|
|
+ /// Slightly wider than default title metrics so the label is not flush to the pill edges.
|
|
|
+ private static let horizontalOutset: CGFloat = 20
|
|
|
+
|
|
|
+ private var trackingAreaRef: NSTrackingArea?
|
|
|
+ private var didPushCursor = false
|
|
|
+
|
|
|
+ init(title: String, target: Any?, action: Selector?) {
|
|
|
+ super.init(frame: .zero)
|
|
|
+ self.title = title
|
|
|
+ self.target = target as AnyObject?
|
|
|
+ self.action = action
|
|
|
+ translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ isBordered = false
|
|
|
+ bezelStyle = .rounded
|
|
|
+ font = .systemFont(ofSize: 13, weight: .semibold)
|
|
|
+ wantsLayer = true
|
|
|
+ layer?.cornerRadius = 8
|
|
|
+ layer?.backgroundColor = Self.fill.cgColor
|
|
|
+ contentTintColor = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
|
|
|
+ focusRingType = .none
|
|
|
+ setContentHuggingPriority(.required, for: .horizontal)
|
|
|
+ setContentCompressionResistancePriority(.required, for: .horizontal)
|
|
|
+ }
|
|
|
+
|
|
|
+ @available(*, unavailable)
|
|
|
+ required init?(coder: NSCoder) {
|
|
|
+ fatalError("init(coder:) has not been implemented")
|
|
|
+ }
|
|
|
+
|
|
|
+ override var intrinsicContentSize: NSSize {
|
|
|
+ let s = super.intrinsicContentSize
|
|
|
+ guard s.width != NSView.noIntrinsicMetric, s.width >= 1 else { return s }
|
|
|
+ return NSSize(width: s.width + Self.horizontalOutset, height: s.height)
|
|
|
+ }
|
|
|
+
|
|
|
+ override func updateTrackingAreas() {
|
|
|
+ super.updateTrackingAreas()
|
|
|
+ if let ta = trackingAreaRef { removeTrackingArea(ta) }
|
|
|
+ let ta = NSTrackingArea(
|
|
|
+ rect: bounds,
|
|
|
+ options: [.activeInKeyWindow, .mouseEnteredAndExited, .inVisibleRect],
|
|
|
+ owner: self,
|
|
|
+ userInfo: nil
|
|
|
+ )
|
|
|
+ addTrackingArea(ta)
|
|
|
+ trackingAreaRef = ta
|
|
|
+ }
|
|
|
+
|
|
|
+ override func mouseEntered(with event: NSEvent) {
|
|
|
+ super.mouseEntered(with: event)
|
|
|
+ layer?.backgroundColor = Self.fillHover.cgColor
|
|
|
+ if !didPushCursor {
|
|
|
+ NSCursor.pointingHand.push()
|
|
|
+ didPushCursor = true
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ override func mouseExited(with event: NSEvent) {
|
|
|
+ super.mouseExited(with: event)
|
|
|
+ layer?.backgroundColor = Self.fill.cgColor
|
|
|
+ if didPushCursor {
|
|
|
+ NSCursor.pop()
|
|
|
+ didPushCursor = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ override func viewWillMove(toWindow newWindow: NSWindow?) {
|
|
|
+ super.viewWillMove(toWindow: newWindow)
|
|
|
+ if newWindow == nil, didPushCursor {
|
|
|
+ NSCursor.pop()
|
|
|
+ didPushCursor = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
/// Hosts a scrollable `CVProfileDocumentView` with a simple chrome header and back navigation.
|
|
|
final class CVFilledPreviewPageView: NSView {
|
|
|
|
|
|
@@ -19,8 +98,7 @@ final class CVFilledPreviewPageView: NSView {
|
|
|
|
|
|
private let backButton = NSButton(title: "← Profiles", target: nil, action: nil)
|
|
|
private let titleLabel = NSTextField(labelWithString: "CV preview")
|
|
|
- private let exportButton = NSButton(title: "Export PDF…", target: nil, action: nil)
|
|
|
- private let editCheckbox = NSButton(checkboxWithTitle: "Edit text in place", target: nil, action: nil)
|
|
|
+ private let exportButton = CVPreviewPrimaryCTAButton(title: "Export PDF…", target: nil, action: nil)
|
|
|
private let scrollView = NSScrollView()
|
|
|
private let documentView = CVPreviewFlippedDocumentView()
|
|
|
private let contentStack = NSStackView()
|
|
|
@@ -50,31 +128,15 @@ final class CVFilledPreviewPageView: NSView {
|
|
|
titleLabel.font = .systemFont(ofSize: 18, weight: .semibold)
|
|
|
titleLabel.textColor = NSColor(srgbRed: 31 / 255, green: 41 / 255, blue: 55 / 255, alpha: 1)
|
|
|
|
|
|
- exportButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
- exportButton.bezelStyle = .rounded
|
|
|
- exportButton.isBordered = false
|
|
|
- exportButton.contentTintColor = Self.brandBlue
|
|
|
- exportButton.font = .systemFont(ofSize: 13, weight: .semibold)
|
|
|
exportButton.target = self
|
|
|
exportButton.action = #selector(didTapExportPDF)
|
|
|
|
|
|
- editCheckbox.translatesAutoresizingMaskIntoConstraints = false
|
|
|
- editCheckbox.font = .systemFont(ofSize: 12, weight: .regular)
|
|
|
- editCheckbox.target = self
|
|
|
- editCheckbox.action = #selector(didToggleEditMode)
|
|
|
-
|
|
|
- let subtitle = NSTextField(wrappingLabelWithString: "Layout matches the CV Maker thumbnail for this template. Turn on editing to adjust wording, then export a PDF that matches what you see here (fonts, columns, colours, and rules).")
|
|
|
+ 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).")
|
|
|
subtitle.font = .systemFont(ofSize: 12, weight: .regular)
|
|
|
subtitle.textColor = Self.secondaryText
|
|
|
subtitle.maximumNumberOfLines = 0
|
|
|
|
|
|
- let actions = NSStackView(views: [exportButton, editCheckbox])
|
|
|
- actions.orientation = .horizontal
|
|
|
- actions.spacing = 16
|
|
|
- actions.alignment = .centerY
|
|
|
- actions.translatesAutoresizingMaskIntoConstraints = false
|
|
|
-
|
|
|
- let headerCol = NSStackView(views: [backButton, titleLabel, subtitle, actions])
|
|
|
+ let headerCol = NSStackView(views: [backButton, titleLabel, subtitle, exportButton])
|
|
|
headerCol.orientation = .vertical
|
|
|
headerCol.alignment = .leading
|
|
|
headerCol.spacing = 6
|
|
|
@@ -106,6 +168,9 @@ final class CVFilledPreviewPageView: NSView {
|
|
|
headerCol.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -32),
|
|
|
headerCol.topAnchor.constraint(equalTo: topAnchor, constant: 16),
|
|
|
|
|
|
+ exportButton.heightAnchor.constraint(equalToConstant: 32),
|
|
|
+ exportButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 96),
|
|
|
+
|
|
|
scrollView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
|
scrollView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
|
scrollView.topAnchor.constraint(equalTo: headerCol.bottomAnchor, constant: 16),
|
|
|
@@ -128,15 +193,14 @@ final class CVFilledPreviewPageView: NSView {
|
|
|
fatalError("init(coder:) has not been implemented")
|
|
|
}
|
|
|
|
|
|
- func configure(profile: SavedProfile, template: CVTemplate, isEditable: Bool = false) {
|
|
|
+ func configure(profile: SavedProfile, template: CVTemplate) {
|
|
|
lastProfile = profile
|
|
|
lastTemplate = template
|
|
|
- editCheckbox.state = isEditable ? .on : .off
|
|
|
for v in contentStack.arrangedSubviews {
|
|
|
contentStack.removeArrangedSubview(v)
|
|
|
v.removeFromSuperview()
|
|
|
}
|
|
|
- let doc = CVProfileDocumentView(profile: profile, template: template, isEditable: isEditable)
|
|
|
+ let doc = CVProfileDocumentView(profile: profile, template: template)
|
|
|
profileDocumentView = doc
|
|
|
contentStack.addArrangedSubview(doc)
|
|
|
let profileTitle = profile.profileDisplayName.isEmpty ? "Untitled profile" : profile.profileDisplayName
|
|
|
@@ -147,11 +211,6 @@ final class CVFilledPreviewPageView: NSView {
|
|
|
onDismiss?()
|
|
|
}
|
|
|
|
|
|
- @objc private func didToggleEditMode(_ sender: NSButton) {
|
|
|
- guard let profile = lastProfile, let template = lastTemplate else { return }
|
|
|
- configure(profile: profile, template: template, isEditable: sender.state == .on)
|
|
|
- }
|
|
|
-
|
|
|
@objc private func didTapExportPDF() {
|
|
|
// NSSavePanel and sandboxed file writes must run on the main thread (and after a button
|
|
|
// callback can be mis-attributed under Swift’s default actor isolation).
|