|
|
@@ -20,47 +20,40 @@ private enum PremiumSheetLayout {
|
|
20
|
20
|
}
|
|
21
|
21
|
|
|
22
|
22
|
final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDelegate, NSSharingServiceDelegate {
|
|
23
|
|
- /// Indeed.com-inspired neutrals and brand blue (white surfaces, `#2557a7` accent, `#2d2d2d` / `#767676` text, `#d4d2d0` borders).
|
|
|
23
|
+ /// Indeed.com-inspired neutrals and brand blue; values follow light / dark / system appearance.
|
|
24
|
24
|
private enum Theme {
|
|
25
|
|
- static let brandBlue = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 1)
|
|
26
|
|
- static let pageBackground = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
|
|
27
|
|
- static let chromeBackground = NSColor(srgbRed: 247 / 255, green: 247 / 255, blue: 247 / 255, alpha: 1)
|
|
28
|
|
- static let sidebarBackground = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
|
|
29
|
|
- static let mainHostBackground = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
|
|
30
|
|
- /// Welcome hero (matches reference: `#0052CC` heading, `#334155` subline, `#EFF6FF` icon well).
|
|
31
|
|
- static let welcomeHeroHeadingBlue = NSColor(srgbRed: 0, green: 82 / 255, blue: 204 / 255, alpha: 1)
|
|
32
|
|
- static let welcomeHeroSubtitleText = NSColor(srgbRed: 51 / 255, green: 65 / 255, blue: 85 / 255, alpha: 1)
|
|
33
|
|
- static let welcomeHeroIconWell = NSColor(srgbRed: 239 / 255, green: 246 / 255, blue: 255 / 255, alpha: 1)
|
|
34
|
|
- /// Light decorative strokes / sparkles behind the welcome hero.
|
|
35
|
|
- static let welcomeHeroWaveTint = NSColor(srgbRed: 186 / 255, green: 210 / 255, blue: 253 / 255, alpha: 1)
|
|
36
|
|
- /// Subtitle on the welcome hero: dark neutral gray to match the reference layout.
|
|
37
|
|
- static let welcomeSubtitleText = NSColor(srgbRed: 64 / 255, green: 64 / 255, blue: 64 / 255, alpha: 1)
|
|
38
|
|
- static let selectionFill = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 0.12)
|
|
39
|
|
- static let cardBackground = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
|
|
40
|
|
- static let toggleBackground = NSColor(srgbRed: 232 / 255, green: 232 / 255, blue: 232 / 255, alpha: 1)
|
|
41
|
|
- static let primaryText = NSColor(srgbRed: 45 / 255, green: 45 / 255, blue: 45 / 255, alpha: 1)
|
|
42
|
|
- static let secondaryText = NSColor(srgbRed: 118 / 255, green: 118 / 255, blue: 118 / 255, alpha: 1)
|
|
43
|
|
- static let tertiaryText = NSColor(srgbRed: 118 / 255, green: 118 / 255, blue: 118 / 255, alpha: 1)
|
|
44
|
|
- static let border = NSColor(srgbRed: 212 / 255, green: 210 / 255, blue: 208 / 255, alpha: 1)
|
|
45
|
|
- static let featureIconWell = NSColor(srgbRed: 220 / 255, green: 235 / 255, blue: 252 / 255, alpha: 1)
|
|
46
|
|
- /// Job search bar outer stroke (soft blue-gray, pill field in the reference UI).
|
|
47
|
|
- static let searchBarBorder = NSColor(srgbRed: 180 / 255, green: 200 / 255, blue: 228 / 255, alpha: 1)
|
|
48
|
|
- /// Search bar border on hover (brand-tinted, matches focus affordance elsewhere).
|
|
49
|
|
- static let searchBarBorderHover = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 0.55)
|
|
50
|
|
- static let proCardFill = NSColor(srgbRed: 239 / 255, green: 244 / 255, blue: 252 / 255, alpha: 1)
|
|
51
|
|
- static let proCardBorder = NSColor(srgbRed: 212 / 255, green: 210 / 255, blue: 208 / 255, alpha: 1)
|
|
52
|
|
- static let proAccent = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 1)
|
|
53
|
|
- static let proCTABackground = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 1)
|
|
54
|
|
- static let proCTAText = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
|
|
55
|
|
- /// Hover states: darker brand blue, stronger tints, and subtle neutral fills used across CTAs, toggles, and the sidebar.
|
|
56
|
|
- static let brandBlueHover = NSColor(srgbRed: 28 / 255, green: 70 / 255, blue: 140 / 255, alpha: 1)
|
|
57
|
|
- static let selectionFillHover = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 0.2)
|
|
58
|
|
- static let neutralHoverFill = NSColor(srgbRed: 240 / 255, green: 240 / 255, blue: 240 / 255, alpha: 1)
|
|
59
|
|
- static let sidebarRowHoverFill = NSColor(srgbRed: 0, green: 0, blue: 0, alpha: 0.04)
|
|
60
|
|
- static let settingsPageBackground = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
|
|
61
|
|
- static let settingsGroupBackground = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
|
|
62
|
|
- static let settingsIconBackground = NSColor(srgbRed: 239 / 255, green: 244 / 255, blue: 252 / 255, alpha: 1)
|
|
63
|
|
- static let settingsDivider = NSColor(srgbRed: 228 / 255, green: 228 / 255, blue: 228 / 255, alpha: 1)
|
|
|
25
|
+ static var brandBlue: NSColor { AppDashboardTheme.brandBlue }
|
|
|
26
|
+ static var pageBackground: NSColor { AppDashboardTheme.pageBackground }
|
|
|
27
|
+ static var chromeBackground: NSColor { AppDashboardTheme.chromeBackground }
|
|
|
28
|
+ static var sidebarBackground: NSColor { AppDashboardTheme.sidebarBackground }
|
|
|
29
|
+ static var mainHostBackground: NSColor { AppDashboardTheme.mainHostBackground }
|
|
|
30
|
+ static var welcomeHeroHeadingBlue: NSColor { AppDashboardTheme.welcomeHeroHeadingBlue }
|
|
|
31
|
+ static var welcomeHeroSubtitleText: NSColor { AppDashboardTheme.welcomeHeroSubtitleText }
|
|
|
32
|
+ static var welcomeHeroIconWell: NSColor { AppDashboardTheme.welcomeHeroIconWell }
|
|
|
33
|
+ static var welcomeHeroWaveTint: NSColor { AppDashboardTheme.welcomeHeroWaveTint }
|
|
|
34
|
+ static var welcomeSubtitleText: NSColor { AppDashboardTheme.welcomeSubtitleText }
|
|
|
35
|
+ static var selectionFill: NSColor { AppDashboardTheme.selectionFill }
|
|
|
36
|
+ static var cardBackground: NSColor { AppDashboardTheme.cardBackground }
|
|
|
37
|
+ static var toggleBackground: NSColor { AppDashboardTheme.toggleBackground }
|
|
|
38
|
+ static var primaryText: NSColor { AppDashboardTheme.primaryText }
|
|
|
39
|
+ static var secondaryText: NSColor { AppDashboardTheme.secondaryText }
|
|
|
40
|
+ static var tertiaryText: NSColor { AppDashboardTheme.tertiaryText }
|
|
|
41
|
+ static var border: NSColor { AppDashboardTheme.border }
|
|
|
42
|
+ static var searchBarBorder: NSColor { AppDashboardTheme.searchBarBorder }
|
|
|
43
|
+ static var searchBarBorderHover: NSColor { AppDashboardTheme.searchBarBorderHover }
|
|
|
44
|
+ static var proCardFill: NSColor { AppDashboardTheme.proCardFill }
|
|
|
45
|
+ static var proCardBorder: NSColor { AppDashboardTheme.proCardBorder }
|
|
|
46
|
+ static var proAccent: NSColor { AppDashboardTheme.proAccent }
|
|
|
47
|
+ static var proCTABackground: NSColor { AppDashboardTheme.proCTABackground }
|
|
|
48
|
+ static var proCTAText: NSColor { AppDashboardTheme.proCTAText }
|
|
|
49
|
+ static var brandBlueHover: NSColor { AppDashboardTheme.brandBlueHover }
|
|
|
50
|
+ static var selectionFillHover: NSColor { AppDashboardTheme.selectionFillHover }
|
|
|
51
|
+ static var neutralHoverFill: NSColor { AppDashboardTheme.neutralHoverFill }
|
|
|
52
|
+ static var sidebarRowHoverFill: NSColor { AppDashboardTheme.sidebarRowHoverFill }
|
|
|
53
|
+ static var settingsPageBackground: NSColor { AppDashboardTheme.settingsPageBackground }
|
|
|
54
|
+ static var settingsGroupBackground: NSColor { AppDashboardTheme.settingsGroupBackground }
|
|
|
55
|
+ static var settingsIconBackground: NSColor { AppDashboardTheme.settingsIconBackground }
|
|
|
56
|
+ static var settingsDivider: NSColor { AppDashboardTheme.settingsDivider }
|
|
64
|
57
|
}
|
|
65
|
58
|
|
|
66
|
59
|
/// Multiline `NSTextField` often ignores `alignment` for wrapped runs; explicit paragraph alignment matches the title.
|
|
|
@@ -163,6 +156,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
|
|
163
|
156
|
private weak var sidebarUpgradeDescription: NSTextField?
|
|
164
|
157
|
private weak var sidebarUpgradeButton: HoverableButton?
|
|
165
|
158
|
private var subscriptionObserver: NSObjectProtocol?
|
|
|
159
|
+ private var appearanceObserver: NSObjectProtocol?
|
|
166
|
160
|
/// Retains the system share picker until the user picks a destination or dismisses the menu.
|
|
167
|
161
|
private var appSharePicker: NSSharingServicePicker?
|
|
168
|
162
|
|
|
|
@@ -178,17 +172,37 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
|
|
178
|
172
|
override init(frame frameRect: NSRect) {
|
|
179
|
173
|
super.init(frame: frameRect)
|
|
180
|
174
|
setupLayout()
|
|
|
175
|
+ registerAppearanceObservers()
|
|
181
|
176
|
}
|
|
182
|
177
|
|
|
183
|
178
|
required init?(coder: NSCoder) {
|
|
184
|
179
|
super.init(coder: coder)
|
|
185
|
180
|
setupLayout()
|
|
|
181
|
+ registerAppearanceObservers()
|
|
186
|
182
|
}
|
|
187
|
183
|
|
|
188
|
184
|
deinit {
|
|
189
|
185
|
if let subscriptionObserver {
|
|
190
|
186
|
NotificationCenter.default.removeObserver(subscriptionObserver)
|
|
191
|
187
|
}
|
|
|
188
|
+ if let appearanceObserver {
|
|
|
189
|
+ NotificationCenter.default.removeObserver(appearanceObserver)
|
|
|
190
|
+ }
|
|
|
191
|
+ }
|
|
|
192
|
+
|
|
|
193
|
+ override func viewDidChangeEffectiveAppearance() {
|
|
|
194
|
+ super.viewDidChangeEffectiveAppearance()
|
|
|
195
|
+ applyCurrentAppearance()
|
|
|
196
|
+ }
|
|
|
197
|
+
|
|
|
198
|
+ private func registerAppearanceObservers() {
|
|
|
199
|
+ appearanceObserver = NotificationCenter.default.addObserver(
|
|
|
200
|
+ forName: AppAppearanceManager.didChangeNotification,
|
|
|
201
|
+ object: nil,
|
|
|
202
|
+ queue: .main
|
|
|
203
|
+ ) { [weak self] _ in
|
|
|
204
|
+ self?.applyCurrentAppearance()
|
|
|
205
|
+ }
|
|
192
|
206
|
}
|
|
193
|
207
|
|
|
194
|
208
|
override func viewDidMoveToWindow() {
|
|
|
@@ -220,6 +234,95 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
|
|
220
|
234
|
savedJobOrder = Self.normalizedSavedJobs(SavedJobsStore.load())
|
|
221
|
235
|
resetChatState()
|
|
222
|
236
|
updateMainContentVisibility()
|
|
|
237
|
+ applyCurrentAppearance()
|
|
|
238
|
+ }
|
|
|
239
|
+
|
|
|
240
|
+ private func applyCurrentAppearance() {
|
|
|
241
|
+ window?.backgroundColor = AppAppearanceManager.shared.windowChromeColor
|
|
|
242
|
+
|
|
|
243
|
+ layer?.backgroundColor = Theme.chromeBackground.cgColor
|
|
|
244
|
+ chromeContainer.layer?.backgroundColor = Theme.chromeBackground.cgColor
|
|
|
245
|
+ sidebar.layer?.backgroundColor = Theme.sidebarBackground.cgColor
|
|
|
246
|
+ mainHost.layer?.backgroundColor = Theme.mainHostBackground.cgColor
|
|
|
247
|
+ indeedJobBrowserHost.layer?.backgroundColor = Theme.mainHostBackground.cgColor
|
|
|
248
|
+ nonHomeHost.layer?.backgroundColor = Theme.mainHostBackground.cgColor
|
|
|
249
|
+ cvMakerPageContainer.layer?.backgroundColor = Theme.mainHostBackground.cgColor
|
|
|
250
|
+ profilePageContainer.layer?.backgroundColor = Theme.mainHostBackground.cgColor
|
|
|
251
|
+ settingsPageContainer.layer?.backgroundColor = Theme.settingsPageBackground.cgColor
|
|
|
252
|
+
|
|
|
253
|
+ greetingLabel.textColor = Theme.welcomeHeroHeadingBlue
|
|
|
254
|
+ subtitleLabel.textColor = Theme.welcomeHeroSubtitleText
|
|
|
255
|
+ welcomeHeroBackgroundView.waveTint = Theme.welcomeHeroWaveTint
|
|
|
256
|
+ welcomeLogoWell.layer?.backgroundColor = Theme.welcomeHeroIconWell.cgColor
|
|
|
257
|
+
|
|
|
258
|
+ nonHomeTitleLabel.textColor = Theme.primaryText
|
|
|
259
|
+ nonHomeSubtitleLabel.textColor = Theme.secondaryText
|
|
|
260
|
+ savedJobsPageTitleLabel.textColor = Theme.primaryText
|
|
|
261
|
+ savedJobsPageSubtitleLabel.textColor = Theme.secondaryText
|
|
|
262
|
+
|
|
|
263
|
+ let searchHovering = searchCard.isHovering
|
|
|
264
|
+ searchCard.layer?.backgroundColor = (searchHovering ? Theme.neutralHoverFill : Theme.cardBackground).cgColor
|
|
|
265
|
+ searchCard.layer?.borderColor = (searchHovering ? Theme.searchBarBorderHover : Theme.searchBarBorder).cgColor
|
|
|
266
|
+ jobKeywordsField.textColor = Theme.primaryText
|
|
|
267
|
+ jobKeywordsField.placeholderAttributedString = NSAttributedString(
|
|
|
268
|
+ string: "Ask for roles, skills, salary, or job descriptions...",
|
|
|
269
|
+ attributes: [
|
|
|
270
|
+ .foregroundColor: Theme.secondaryText,
|
|
|
271
|
+ .font: NSFont.systemFont(ofSize: 14, weight: .regular)
|
|
|
272
|
+ ]
|
|
|
273
|
+ )
|
|
|
274
|
+ jobSearchIcon.contentTintColor = Theme.brandBlue
|
|
|
275
|
+ let ctaHovering = findJobsButton.isHovering
|
|
|
276
|
+ findJobsCTAPill.layer?.backgroundColor = (ctaHovering ? Theme.brandBlueHover : Theme.brandBlue).cgColor
|
|
|
277
|
+ sendIconView.contentTintColor = Theme.proCTAText
|
|
|
278
|
+ sendLabel.textColor = Theme.proCTAText
|
|
|
279
|
+
|
|
|
280
|
+ appearanceModeSegment?.selectedSegment = AppAppearanceManager.shared.mode.segmentIndex
|
|
|
281
|
+ refreshSettingsPageAppearance(in: settingsPageContainer)
|
|
|
282
|
+ rebuildFeatureShortcutCards()
|
|
|
283
|
+ configureSidebar()
|
|
|
284
|
+ reloadSavedJobsListings()
|
|
|
285
|
+ rebuildChatUI()
|
|
|
286
|
+ applyProSubscriptionToSidebar()
|
|
|
287
|
+ needsLayout = true
|
|
|
288
|
+ }
|
|
|
289
|
+
|
|
|
290
|
+ private func refreshSettingsPageAppearance(in root: NSView) {
|
|
|
291
|
+ for case let field as NSTextField in root.subviewsRecursive() where !field.isEditable {
|
|
|
292
|
+ switch field.font?.pointSize {
|
|
|
293
|
+ case 12, 14:
|
|
|
294
|
+ field.textColor = Theme.secondaryText
|
|
|
295
|
+ default:
|
|
|
296
|
+ break
|
|
|
297
|
+ }
|
|
|
298
|
+ }
|
|
|
299
|
+ for case let stack as NSStackView in root.subviewsRecursive() where stack.orientation == .vertical && stack.layer?.cornerRadius == 14 {
|
|
|
300
|
+ stack.layer?.backgroundColor = Theme.settingsGroupBackground.cgColor
|
|
|
301
|
+ stack.layer?.borderColor = Theme.border.cgColor
|
|
|
302
|
+ for case let divider as NSView in stack.arrangedSubviews where divider.bounds.height <= 1.5 && divider.wantsLayer {
|
|
|
303
|
+ divider.layer?.backgroundColor = Theme.settingsDivider.cgColor
|
|
|
304
|
+ }
|
|
|
305
|
+ }
|
|
|
306
|
+ for case let tile as NSView in root.subviewsRecursive() where tile.layer?.cornerRadius == 9 && tile.bounds.width == 38 {
|
|
|
307
|
+ tile.layer?.backgroundColor = Theme.settingsIconBackground.cgColor
|
|
|
308
|
+ for case let icon as NSImageView in tile.subviews {
|
|
|
309
|
+ icon.contentTintColor = Theme.brandBlue
|
|
|
310
|
+ }
|
|
|
311
|
+ }
|
|
|
312
|
+ }
|
|
|
313
|
+
|
|
|
314
|
+ private func rebuildFeatureShortcutCards() {
|
|
|
315
|
+ let selectedIndex = featureCardsRow.arrangedSubviews.firstIndex {
|
|
|
316
|
+ ($0 as? FeatureShortcutCardView)?.isSelected == true
|
|
|
317
|
+ }
|
|
|
318
|
+ featureCardsRow.arrangedSubviews.forEach {
|
|
|
319
|
+ featureCardsRow.removeArrangedSubview($0)
|
|
|
320
|
+ $0.removeFromSuperview()
|
|
|
321
|
+ }
|
|
|
322
|
+ configureFeatureShortcutCards()
|
|
|
323
|
+ if let selectedIndex, let shortcut = FeatureShortcut(rawValue: selectedIndex) {
|
|
|
324
|
+ selectFeatureShortcut(shortcut)
|
|
|
325
|
+ }
|
|
223
|
326
|
}
|
|
224
|
327
|
|
|
225
|
328
|
private func setupLayout() {
|
|
|
@@ -2039,7 +2142,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
|
|
2039
|
2142
|
newJobsCount: freshJobs.count,
|
|
2040
|
2143
|
isContinuation: isContinuation
|
|
2041
|
2144
|
)
|
|
2042
|
|
- self.chatMessages.append(ChatMessage(role: "assistant", content: reply))
|
|
|
2145
|
+ self.chatMessages.append(ChatMessage(role: "assistant", content: reply, attachedJobs: freshJobs.isEmpty ? nil : freshJobs))
|
|
2043
|
2146
|
self.appendChatBubble(text: reply, isUser: false, jobs: freshJobs)
|
|
2044
|
2147
|
case .failure(let error):
|
|
2045
|
2148
|
self.appendChatBubble(text: error.localizedDescription, isUser: false)
|
|
|
@@ -2175,13 +2278,33 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
|
|
2175
|
2278
|
trailingLoadMoreJobsButton = nil
|
|
2176
|
2279
|
chatMessages.removeAll()
|
|
2177
|
2280
|
lastSearchResults.removeAll()
|
|
|
2281
|
+ clearChatStack()
|
|
|
2282
|
+ let welcome = "Tell me what role you want and I will return job descriptions, key skills, and a quick fit summary."
|
|
|
2283
|
+ chatMessages.append(ChatMessage(role: "assistant", content: welcome))
|
|
|
2284
|
+ appendChatBubble(text: welcome, isUser: false)
|
|
|
2285
|
+ }
|
|
|
2286
|
+
|
|
|
2287
|
+ private func clearChatStack() {
|
|
2178
|
2288
|
chatStack.arrangedSubviews.forEach {
|
|
2179
|
2289
|
chatStack.removeArrangedSubview($0)
|
|
2180
|
2290
|
$0.removeFromSuperview()
|
|
2181
|
2291
|
}
|
|
2182
|
|
- let welcome = "Tell me what role you want and I will return job descriptions, key skills, and a quick fit summary."
|
|
2183
|
|
- chatMessages.append(ChatMessage(role: "assistant", content: welcome))
|
|
2184
|
|
- appendChatBubble(text: welcome, isUser: false)
|
|
|
2292
|
+ }
|
|
|
2293
|
+
|
|
|
2294
|
+ private func rebuildChatUI() {
|
|
|
2295
|
+ guard !chatMessages.isEmpty else { return }
|
|
|
2296
|
+ removeInlineChatThinkingRow()
|
|
|
2297
|
+ trailingLoadMoreJobsRow = nil
|
|
|
2298
|
+ trailingLoadMoreJobsButton = nil
|
|
|
2299
|
+ clearChatStack()
|
|
|
2300
|
+ for message in chatMessages {
|
|
|
2301
|
+ let isUser = message.role == "user"
|
|
|
2302
|
+ appendChatBubble(text: message.content, isUser: isUser, jobs: message.attachedJobs)
|
|
|
2303
|
+ }
|
|
|
2304
|
+ for host in chatStack.arrangedSubviews {
|
|
|
2305
|
+ host.alphaValue = 1
|
|
|
2306
|
+ }
|
|
|
2307
|
+ updateChatBubbleWidths()
|
|
2185
|
2308
|
}
|
|
2186
|
2309
|
|
|
2187
|
2310
|
private func appendChatBubble(text: String, isUser: Bool, jobs: [JobListing]? = nil) {
|
|
|
@@ -2741,6 +2864,31 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
|
|
2741
|
2864
|
private struct ChatMessage: Codable {
|
|
2742
|
2865
|
let role: String
|
|
2743
|
2866
|
let content: String
|
|
|
2867
|
+ /// Job cards shown under this assistant message (not sent to the API).
|
|
|
2868
|
+ var attachedJobs: [JobListing]?
|
|
|
2869
|
+
|
|
|
2870
|
+ enum CodingKeys: String, CodingKey {
|
|
|
2871
|
+ case role
|
|
|
2872
|
+ case content
|
|
|
2873
|
+ }
|
|
|
2874
|
+
|
|
|
2875
|
+ init(role: String, content: String, attachedJobs: [JobListing]? = nil) {
|
|
|
2876
|
+ self.role = role
|
|
|
2877
|
+ self.content = content
|
|
|
2878
|
+ self.attachedJobs = attachedJobs
|
|
|
2879
|
+ }
|
|
|
2880
|
+}
|
|
|
2881
|
+
|
|
|
2882
|
+private extension NSView {
|
|
|
2883
|
+ func subviewsRecursive() -> [NSView] {
|
|
|
2884
|
+ var result: [NSView] = []
|
|
|
2885
|
+ var stack: [NSView] = [self]
|
|
|
2886
|
+ while let view = stack.popLast() {
|
|
|
2887
|
+ result.append(view)
|
|
|
2888
|
+ stack.append(contentsOf: view.subviews)
|
|
|
2889
|
+ }
|
|
|
2890
|
+ return result
|
|
|
2891
|
+ }
|
|
2744
|
2892
|
}
|
|
2745
|
2893
|
|
|
2746
|
2894
|
private final class OpenAIJobSearchService {
|
|
|
@@ -3303,9 +3451,12 @@ private final class WelcomeHeroBackgroundView: NSView {
|
|
3303
|
3451
|
/// Home welcome row: three tappable shortcuts that seed the main search field (reference: white cards, pastel icon well, arrow at bottom trailing).
|
|
3304
|
3452
|
private final class FeatureShortcutCardView: NSView {
|
|
3305
|
3453
|
private static let cardCornerRadius: CGFloat = 14
|
|
3306
|
|
- private static let primaryBlue = NSColor(srgbRed: 0, green: 82 / 255, blue: 204 / 255, alpha: 1)
|
|
3307
|
|
- private static let defaultBorderColor = NSColor(srgbRed: 237 / 255, green: 242 / 255, blue: 247 / 255, alpha: 1)
|
|
3308
|
3454
|
var isSelected = false { didSet { updateSelectionAppearance() } }
|
|
|
3455
|
+ private weak var titleField: NSTextField?
|
|
|
3456
|
+ private weak var subtitleField: NSTextField?
|
|
|
3457
|
+ private weak var iconHost: NSView?
|
|
|
3458
|
+ private weak var iconView: NSImageView?
|
|
|
3459
|
+ private weak var chevronView: NSImageView?
|
|
3309
|
3460
|
private weak var actionTarget: AnyObject?
|
|
3310
|
3461
|
private var actionSelector: Selector
|
|
3311
|
3462
|
|
|
|
@@ -3319,37 +3470,31 @@ private final class FeatureShortcutCardView: NSView {
|
|
3319
|
3470
|
if #available(macOS 11.0, *) {
|
|
3320
|
3471
|
layer?.cornerCurve = .continuous
|
|
3321
|
3472
|
}
|
|
3322
|
|
- layer?.backgroundColor = NSColor.white.cgColor
|
|
|
3473
|
+ layer?.backgroundColor = AppDashboardTheme.featureCardBackground.cgColor
|
|
3323
|
3474
|
layer?.masksToBounds = false
|
|
3324
|
|
- layer?.shadowColor = NSColor.black.withAlphaComponent(0.06).cgColor
|
|
|
3475
|
+ layer?.shadowColor = NSColor.black.withAlphaComponent(AppDashboardTheme.isDark ? 0.35 : 0.06).cgColor
|
|
3325
|
3476
|
layer?.shadowOffset = CGSize(width: 0, height: 2)
|
|
3326
|
3477
|
layer?.shadowRadius = 12
|
|
3327
|
3478
|
layer?.shadowOpacity = 1
|
|
3328
|
3479
|
updateSelectionAppearance()
|
|
3329
|
3480
|
|
|
3330
|
|
- let primaryBlue = Self.primaryBlue
|
|
3331
|
|
- // `#EBF2FF` — circular icon well.
|
|
3332
|
|
- let iconWellColor = NSColor(srgbRed: 235 / 255, green: 242 / 255, blue: 255 / 255, alpha: 1)
|
|
3333
|
|
- // `#5D6D7E` — muted description.
|
|
3334
|
|
- let secondary = NSColor(srgbRed: 93 / 255, green: 109 / 255, blue: 126 / 255, alpha: 1)
|
|
3335
|
|
-
|
|
3336
|
3481
|
let iconSize: CGFloat = 40
|
|
3337
|
3482
|
let iconHost = NSView()
|
|
|
3483
|
+ self.iconHost = iconHost
|
|
3338
|
3484
|
iconHost.translatesAutoresizingMaskIntoConstraints = false
|
|
3339
|
3485
|
iconHost.wantsLayer = true
|
|
3340
|
|
- iconHost.layer?.backgroundColor = iconWellColor.cgColor
|
|
3341
|
3486
|
iconHost.layer?.cornerRadius = iconSize / 2
|
|
3342
|
3487
|
|
|
3343
|
3488
|
let icon = NSImageView()
|
|
|
3489
|
+ self.iconView = icon
|
|
3344
|
3490
|
icon.translatesAutoresizingMaskIntoConstraints = false
|
|
3345
|
3491
|
icon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 16, weight: .regular)
|
|
3346
|
3492
|
icon.image = NSImage(systemSymbolName: symbolName, accessibilityDescription: nil)
|
|
3347
|
|
- icon.contentTintColor = primaryBlue
|
|
3348
|
3493
|
iconHost.addSubview(icon)
|
|
3349
|
3494
|
|
|
3350
|
3495
|
let titleField = NSTextField(wrappingLabelWithString: title)
|
|
|
3496
|
+ self.titleField = titleField
|
|
3351
|
3497
|
titleField.font = .systemFont(ofSize: 15, weight: .bold)
|
|
3352
|
|
- titleField.textColor = primaryBlue
|
|
3353
|
3498
|
titleField.maximumNumberOfLines = 1
|
|
3354
|
3499
|
titleField.isEditable = false
|
|
3355
|
3500
|
titleField.isBordered = false
|
|
|
@@ -3357,8 +3502,8 @@ private final class FeatureShortcutCardView: NSView {
|
|
3357
|
3502
|
titleField.alignment = .left
|
|
3358
|
3503
|
|
|
3359
|
3504
|
let subtitleField = NSTextField(wrappingLabelWithString: subtitle)
|
|
|
3505
|
+ self.subtitleField = subtitleField
|
|
3360
|
3506
|
subtitleField.font = .systemFont(ofSize: 12, weight: .regular)
|
|
3361
|
|
- subtitleField.textColor = secondary
|
|
3362
|
3507
|
subtitleField.maximumNumberOfLines = 2
|
|
3363
|
3508
|
subtitleField.isEditable = false
|
|
3364
|
3509
|
subtitleField.isBordered = false
|
|
|
@@ -3369,10 +3514,10 @@ private final class FeatureShortcutCardView: NSView {
|
|
3369
|
3514
|
subtitleField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
3370
|
3515
|
|
|
3371
|
3516
|
let chevron = NSImageView()
|
|
|
3517
|
+ self.chevronView = chevron
|
|
3372
|
3518
|
chevron.translatesAutoresizingMaskIntoConstraints = false
|
|
3373
|
3519
|
chevron.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 12, weight: .semibold)
|
|
3374
|
3520
|
chevron.image = NSImage(systemSymbolName: "arrow.right", accessibilityDescription: nil)
|
|
3375
|
|
- chevron.contentTintColor = primaryBlue
|
|
3376
|
3521
|
chevron.setContentHuggingPriority(.required, for: .horizontal)
|
|
3377
|
3522
|
chevron.setContentCompressionResistancePriority(.required, for: .horizontal)
|
|
3378
|
3523
|
|
|
|
@@ -3418,17 +3563,36 @@ private final class FeatureShortcutCardView: NSView {
|
|
3418
|
3563
|
setAccessibilityElement(true)
|
|
3419
|
3564
|
setAccessibilityRole(.button)
|
|
3420
|
3565
|
setAccessibilityLabel("\(title). \(subtitle)")
|
|
|
3566
|
+ applyCardAppearance()
|
|
|
3567
|
+ }
|
|
|
3568
|
+
|
|
|
3569
|
+ func applyCardAppearance() {
|
|
|
3570
|
+ layer?.backgroundColor = AppDashboardTheme.featureCardBackground.cgColor
|
|
|
3571
|
+ layer?.shadowColor = NSColor.black.withAlphaComponent(AppDashboardTheme.isDark ? 0.35 : 0.06).cgColor
|
|
|
3572
|
+ iconHost?.layer?.backgroundColor = AppDashboardTheme.featureIconWell.cgColor
|
|
|
3573
|
+ let accent = AppDashboardTheme.featurePrimaryBlue
|
|
|
3574
|
+ iconView?.contentTintColor = accent
|
|
|
3575
|
+ titleField?.textColor = accent
|
|
|
3576
|
+ subtitleField?.textColor = AppDashboardTheme.featureSecondaryText
|
|
|
3577
|
+ chevronView?.contentTintColor = accent
|
|
|
3578
|
+ updateSelectionAppearance()
|
|
|
3579
|
+ }
|
|
|
3580
|
+
|
|
|
3581
|
+ override func viewDidChangeEffectiveAppearance() {
|
|
|
3582
|
+ super.viewDidChangeEffectiveAppearance()
|
|
|
3583
|
+ applyCardAppearance()
|
|
3421
|
3584
|
}
|
|
3422
|
3585
|
|
|
3423
|
3586
|
private func updateSelectionAppearance() {
|
|
3424
|
3587
|
guard let layer else { return }
|
|
|
3588
|
+ let accent = AppDashboardTheme.featurePrimaryBlue
|
|
3425
|
3589
|
if isSelected {
|
|
3426
|
3590
|
layer.borderWidth = 2
|
|
3427
|
|
- layer.borderColor = Self.primaryBlue.cgColor
|
|
3428
|
|
- layer.shadowOpacity = 0.1
|
|
|
3591
|
+ layer.borderColor = accent.cgColor
|
|
|
3592
|
+ layer.shadowOpacity = AppDashboardTheme.isDark ? 0.2 : 0.1
|
|
3429
|
3593
|
} else {
|
|
3430
|
3594
|
layer.borderWidth = 1
|
|
3431
|
|
- layer.borderColor = Self.defaultBorderColor.cgColor
|
|
|
3595
|
+ layer.borderColor = AppDashboardTheme.featureCardBorder.cgColor
|
|
3432
|
3596
|
layer.shadowOpacity = 1
|
|
3433
|
3597
|
}
|
|
3434
|
3598
|
setAccessibilitySelected(isSelected)
|