|
|
@@ -12,6 +12,85 @@ private final class CVPreviewFlippedDocumentView: NSView {
|
|
12
|
12
|
override var isFlipped: Bool { true }
|
|
13
|
13
|
}
|
|
14
|
14
|
|
|
|
15
|
+/// Same metrics as job cards’ **Apply** (`DashboardView` `JobPayloadButton`): 13pt semibold, 32pt tall, 8pt corners.
|
|
|
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)
|
|
|
19
|
+ /// Slightly wider than default title metrics so the label is not flush to the pill edges.
|
|
|
20
|
+ private static let horizontalOutset: CGFloat = 20
|
|
|
21
|
+
|
|
|
22
|
+ private var trackingAreaRef: NSTrackingArea?
|
|
|
23
|
+ private var didPushCursor = false
|
|
|
24
|
+
|
|
|
25
|
+ init(title: String, target: Any?, action: Selector?) {
|
|
|
26
|
+ super.init(frame: .zero)
|
|
|
27
|
+ self.title = title
|
|
|
28
|
+ self.target = target as AnyObject?
|
|
|
29
|
+ self.action = action
|
|
|
30
|
+ translatesAutoresizingMaskIntoConstraints = false
|
|
|
31
|
+ isBordered = false
|
|
|
32
|
+ bezelStyle = .rounded
|
|
|
33
|
+ font = .systemFont(ofSize: 13, weight: .semibold)
|
|
|
34
|
+ wantsLayer = true
|
|
|
35
|
+ layer?.cornerRadius = 8
|
|
|
36
|
+ layer?.backgroundColor = Self.fill.cgColor
|
|
|
37
|
+ contentTintColor = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
|
|
|
38
|
+ focusRingType = .none
|
|
|
39
|
+ setContentHuggingPriority(.required, for: .horizontal)
|
|
|
40
|
+ setContentCompressionResistancePriority(.required, for: .horizontal)
|
|
|
41
|
+ }
|
|
|
42
|
+
|
|
|
43
|
+ @available(*, unavailable)
|
|
|
44
|
+ required init?(coder: NSCoder) {
|
|
|
45
|
+ fatalError("init(coder:) has not been implemented")
|
|
|
46
|
+ }
|
|
|
47
|
+
|
|
|
48
|
+ override var intrinsicContentSize: NSSize {
|
|
|
49
|
+ let s = super.intrinsicContentSize
|
|
|
50
|
+ guard s.width != NSView.noIntrinsicMetric, s.width >= 1 else { return s }
|
|
|
51
|
+ return NSSize(width: s.width + Self.horizontalOutset, height: s.height)
|
|
|
52
|
+ }
|
|
|
53
|
+
|
|
|
54
|
+ override func updateTrackingAreas() {
|
|
|
55
|
+ super.updateTrackingAreas()
|
|
|
56
|
+ if let ta = trackingAreaRef { removeTrackingArea(ta) }
|
|
|
57
|
+ let ta = NSTrackingArea(
|
|
|
58
|
+ rect: bounds,
|
|
|
59
|
+ options: [.activeInKeyWindow, .mouseEnteredAndExited, .inVisibleRect],
|
|
|
60
|
+ owner: self,
|
|
|
61
|
+ userInfo: nil
|
|
|
62
|
+ )
|
|
|
63
|
+ addTrackingArea(ta)
|
|
|
64
|
+ trackingAreaRef = ta
|
|
|
65
|
+ }
|
|
|
66
|
+
|
|
|
67
|
+ override func mouseEntered(with event: NSEvent) {
|
|
|
68
|
+ super.mouseEntered(with: event)
|
|
|
69
|
+ layer?.backgroundColor = Self.fillHover.cgColor
|
|
|
70
|
+ if !didPushCursor {
|
|
|
71
|
+ NSCursor.pointingHand.push()
|
|
|
72
|
+ didPushCursor = true
|
|
|
73
|
+ }
|
|
|
74
|
+ }
|
|
|
75
|
+
|
|
|
76
|
+ override func mouseExited(with event: NSEvent) {
|
|
|
77
|
+ super.mouseExited(with: event)
|
|
|
78
|
+ layer?.backgroundColor = Self.fill.cgColor
|
|
|
79
|
+ if didPushCursor {
|
|
|
80
|
+ NSCursor.pop()
|
|
|
81
|
+ didPushCursor = false
|
|
|
82
|
+ }
|
|
|
83
|
+ }
|
|
|
84
|
+
|
|
|
85
|
+ override func viewWillMove(toWindow newWindow: NSWindow?) {
|
|
|
86
|
+ super.viewWillMove(toWindow: newWindow)
|
|
|
87
|
+ if newWindow == nil, didPushCursor {
|
|
|
88
|
+ NSCursor.pop()
|
|
|
89
|
+ didPushCursor = false
|
|
|
90
|
+ }
|
|
|
91
|
+ }
|
|
|
92
|
+}
|
|
|
93
|
+
|
|
15
|
94
|
/// Hosts a scrollable `CVProfileDocumentView` with a simple chrome header and back navigation.
|
|
16
|
95
|
final class CVFilledPreviewPageView: NSView {
|
|
17
|
96
|
|
|
|
@@ -19,8 +98,7 @@ final class CVFilledPreviewPageView: NSView {
|
|
19
|
98
|
|
|
20
|
99
|
private let backButton = NSButton(title: "← Profiles", target: nil, action: nil)
|
|
21
|
100
|
private let titleLabel = NSTextField(labelWithString: "CV preview")
|
|
22
|
|
- private let exportButton = NSButton(title: "Export PDF…", target: nil, action: nil)
|
|
23
|
|
- private let editCheckbox = NSButton(checkboxWithTitle: "Edit text in place", target: nil, action: nil)
|
|
|
101
|
+ private let exportButton = CVPreviewPrimaryCTAButton(title: "Export PDF…", target: nil, action: nil)
|
|
24
|
102
|
private let scrollView = NSScrollView()
|
|
25
|
103
|
private let documentView = CVPreviewFlippedDocumentView()
|
|
26
|
104
|
private let contentStack = NSStackView()
|
|
|
@@ -50,31 +128,15 @@ final class CVFilledPreviewPageView: NSView {
|
|
50
|
128
|
titleLabel.font = .systemFont(ofSize: 18, weight: .semibold)
|
|
51
|
129
|
titleLabel.textColor = NSColor(srgbRed: 31 / 255, green: 41 / 255, blue: 55 / 255, alpha: 1)
|
|
52
|
130
|
|
|
53
|
|
- exportButton.translatesAutoresizingMaskIntoConstraints = false
|
|
54
|
|
- exportButton.bezelStyle = .rounded
|
|
55
|
|
- exportButton.isBordered = false
|
|
56
|
|
- exportButton.contentTintColor = Self.brandBlue
|
|
57
|
|
- exportButton.font = .systemFont(ofSize: 13, weight: .semibold)
|
|
58
|
131
|
exportButton.target = self
|
|
59
|
132
|
exportButton.action = #selector(didTapExportPDF)
|
|
60
|
133
|
|
|
61
|
|
- editCheckbox.translatesAutoresizingMaskIntoConstraints = false
|
|
62
|
|
- editCheckbox.font = .systemFont(ofSize: 12, weight: .regular)
|
|
63
|
|
- editCheckbox.target = self
|
|
64
|
|
- editCheckbox.action = #selector(didToggleEditMode)
|
|
65
|
|
-
|
|
66
|
|
- 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).")
|
|
|
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).")
|
|
67
|
135
|
subtitle.font = .systemFont(ofSize: 12, weight: .regular)
|
|
68
|
136
|
subtitle.textColor = Self.secondaryText
|
|
69
|
137
|
subtitle.maximumNumberOfLines = 0
|
|
70
|
138
|
|
|
71
|
|
- let actions = NSStackView(views: [exportButton, editCheckbox])
|
|
72
|
|
- actions.orientation = .horizontal
|
|
73
|
|
- actions.spacing = 16
|
|
74
|
|
- actions.alignment = .centerY
|
|
75
|
|
- actions.translatesAutoresizingMaskIntoConstraints = false
|
|
76
|
|
-
|
|
77
|
|
- let headerCol = NSStackView(views: [backButton, titleLabel, subtitle, actions])
|
|
|
139
|
+ let headerCol = NSStackView(views: [backButton, titleLabel, subtitle, exportButton])
|
|
78
|
140
|
headerCol.orientation = .vertical
|
|
79
|
141
|
headerCol.alignment = .leading
|
|
80
|
142
|
headerCol.spacing = 6
|
|
|
@@ -106,6 +168,9 @@ final class CVFilledPreviewPageView: NSView {
|
|
106
|
168
|
headerCol.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -32),
|
|
107
|
169
|
headerCol.topAnchor.constraint(equalTo: topAnchor, constant: 16),
|
|
108
|
170
|
|
|
|
171
|
+ exportButton.heightAnchor.constraint(equalToConstant: 32),
|
|
|
172
|
+ exportButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 96),
|
|
|
173
|
+
|
|
109
|
174
|
scrollView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
110
|
175
|
scrollView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
111
|
176
|
scrollView.topAnchor.constraint(equalTo: headerCol.bottomAnchor, constant: 16),
|
|
|
@@ -128,15 +193,14 @@ final class CVFilledPreviewPageView: NSView {
|
|
128
|
193
|
fatalError("init(coder:) has not been implemented")
|
|
129
|
194
|
}
|
|
130
|
195
|
|
|
131
|
|
- func configure(profile: SavedProfile, template: CVTemplate, isEditable: Bool = false) {
|
|
|
196
|
+ func configure(profile: SavedProfile, template: CVTemplate) {
|
|
132
|
197
|
lastProfile = profile
|
|
133
|
198
|
lastTemplate = template
|
|
134
|
|
- editCheckbox.state = isEditable ? .on : .off
|
|
135
|
199
|
for v in contentStack.arrangedSubviews {
|
|
136
|
200
|
contentStack.removeArrangedSubview(v)
|
|
137
|
201
|
v.removeFromSuperview()
|
|
138
|
202
|
}
|
|
139
|
|
- let doc = CVProfileDocumentView(profile: profile, template: template, isEditable: isEditable)
|
|
|
203
|
+ let doc = CVProfileDocumentView(profile: profile, template: template)
|
|
140
|
204
|
profileDocumentView = doc
|
|
141
|
205
|
contentStack.addArrangedSubview(doc)
|
|
142
|
206
|
let profileTitle = profile.profileDisplayName.isEmpty ? "Untitled profile" : profile.profileDisplayName
|
|
|
@@ -147,11 +211,6 @@ final class CVFilledPreviewPageView: NSView {
|
|
147
|
211
|
onDismiss?()
|
|
148
|
212
|
}
|
|
149
|
213
|
|
|
150
|
|
- @objc private func didToggleEditMode(_ sender: NSButton) {
|
|
151
|
|
- guard let profile = lastProfile, let template = lastTemplate else { return }
|
|
152
|
|
- configure(profile: profile, template: template, isEditable: sender.state == .on)
|
|
153
|
|
- }
|
|
154
|
|
-
|
|
155
|
214
|
@objc private func didTapExportPDF() {
|
|
156
|
215
|
// NSSavePanel and sandboxed file writes must run on the main thread (and after a button
|
|
157
|
216
|
// callback can be mis-attributed under Swift’s default actor isolation).
|