Pārlūkot izejas kodu

Align CV preview export with job-card CTAs and simplify the preview.

Style Export PDF like Apply (32pt height, 8pt corners) with a bit of extra width for the label, and remove in-place editing from the filled preview and document view.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 3 nedēļas atpakaļ
vecāks
revīzija
8836c65012

+ 87 - 28
App for Indeed/Views/CVFilledPreviewPageView.swift

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

+ 6 - 23
App for Indeed/Views/CVProfileDocumentView.swift

@@ -181,8 +181,8 @@ private struct DocumentStyle {
181 181
 final class CVProfileDocumentView: NSView {
182 182
 
183 183
     /// Card width used in the CV preview; also the horizontal fitting size for this view.
184
-    /// Without this, a parent `NSStackView` that only pins `width ≤ …` sizes the document from
185
-    /// editable `NSTextField` intrinsic widths (~0) and the whole page collapses to a thin strip.
184
+    /// Without this, a parent `NSStackView` that only pins `width ≤ …` can size the document from
185
+    /// wrapping `NSTextField` intrinsic widths (~0) and the whole page collapses to a thin strip.
186 186
     private static let cardWidth: CGFloat = 640
187 187
 
188 188
     private let profile: SavedProfile
@@ -190,20 +190,17 @@ final class CVProfileDocumentView: NSView {
190 190
     private let style: DocumentStyle
191 191
     /// Matches `CVTemplatePreviewView` so the same template id + layout recipe renders the same silhouette as the gallery card.
192 192
     private let variant: Int
193
-    private let isEditable: Bool
194
-
195
-    init(profile: SavedProfile, template: CVTemplate, isEditable: Bool = false) {
193
+    init(profile: SavedProfile, template: CVTemplate) {
196 194
         self.profile = profile
197 195
         self.template = template
198 196
         self.style = DocumentStyle.make(for: template)
199 197
         self.variant = template.galleryLayoutVariant
200
-        self.isEditable = isEditable
201 198
         super.init(frame: .zero)
202 199
         translatesAutoresizingMaskIntoConstraints = false
203 200
         wantsLayer = true
204 201
         layer?.backgroundColor = NSColor.clear.cgColor
205 202
         userInterfaceLayoutDirection = .leftToRight
206
-        // Let the preview stack stretch us to the scroll view width; don’t shrink to editable fields.
203
+        // Let the preview stack stretch us to the scroll view width; don’t shrink to label intrinsic widths.
207 204
         setContentHuggingPriority(.defaultLow, for: .horizontal)
208 205
 
209 206
         let card = NSView()
@@ -245,9 +242,8 @@ final class CVProfileDocumentView: NSView {
245 242
 
246 243
     override func layout() {
247 244
         super.layout()
248
-        // Editable wrapping `NSTextField`s default to a very small intrinsic width until
249
-        // `preferredMaxLayoutWidth` tracks the column width — stacks then collapse and text
250
-        // reflows like a narrow strip (broken CV layout in “Edit text in place” mode).
245
+        // Wrapping `NSTextField`s default to a very small intrinsic width until
246
+        // `preferredMaxLayoutWidth` tracks the column width — stacks then collapse and text reflows like a narrow strip.
251 247
         updateWrappingTextPreferredWidths()
252 248
     }
253 249
 
@@ -1264,19 +1260,6 @@ final class CVProfileDocumentView: NSView {
1264 1260
         t.font = font
1265 1261
         t.textColor = color
1266 1262
         t.alignment = .left
1267
-        if isEditable {
1268
-            t.isEditable = true
1269
-            t.isSelectable = true
1270
-            t.isBordered = false
1271
-            t.drawsBackground = false
1272
-            t.focusRingType = .default
1273
-            t.usesSingleLineMode = false
1274
-            if isWrapping, let cell = t.cell as? NSTextFieldCell {
1275
-                cell.wraps = true
1276
-                cell.isScrollable = false
1277
-            }
1278
-            t.setContentHuggingPriority(.defaultLow, for: .horizontal)
1279
-        }
1280 1263
         return t
1281 1264
     }
1282 1265