// // DashboardView.swift // App for Indeed // import Cocoa import QuartzCore final class DashboardView: NSView { private enum Theme { static let pageBackground = NSColor(calibratedWhite: 0.02, alpha: 1) static let chromeBackground = NSColor(calibratedWhite: 0.04, alpha: 1) static let sidebarBackground = NSColor(calibratedWhite: 0.07, alpha: 1) static let selectionFill = NSColor(calibratedWhite: 1, alpha: 0.1) static let cardBackground = NSColor(calibratedWhite: 0.11, alpha: 1) static let toggleBackground = NSColor(calibratedWhite: 0.16, alpha: 1) static let primaryText = NSColor(calibratedWhite: 0.96, alpha: 1) static let secondaryText = NSColor(calibratedWhite: 0.62, alpha: 1) static let tertiaryText = NSColor(calibratedWhite: 0.48, alpha: 1) static let linkText = NSColor(calibratedWhite: 0.82, alpha: 1) } private let contentStack = NSStackView() private let documentContainer = NSView() private let chromeContainer = NSView() private let sidebar = NSStackView() private let mainHost = NSView() private let mainGradient = NeutralGradientBackgroundView() private let mainOverlay = NSStackView() private let greetingLabel = NSTextField(labelWithString: "") private let subtitleLabel = NSTextField(labelWithString: "") private let insightsCard = NSView() private let insightsTitleLabel = NSTextField(labelWithString: "") private let insightsBodyLabel = NSTextField(labelWithString: "") private let insightsLinkButton = NSButton(title: "", target: nil, action: nil) private let togglesLabel = NSTextField(labelWithString: "Dashboard Toggles:") private let savedToggleButton = NSButton(title: "Saved", target: nil, action: nil) private let interviewsToggleButton = NSButton(title: "Interviews", target: nil, action: nil) private let sparkleView = NSImageView() private let scrollView = NSScrollView() override init(frame frameRect: NSRect) { super.init(frame: frameRect) setupLayout() } required init?(coder: NSCoder) { super.init(coder: coder) setupLayout() } override func layout() { super.layout() updateDocumentLayout() } func render(_ data: DashboardData) { greetingLabel.stringValue = "Welcome back, \(data.greetingName)! 👋" subtitleLabel.stringValue = data.subtitle insightsTitleLabel.stringValue = data.profileInsightsTitle insightsBodyLabel.stringValue = data.profileInsightsBody insightsLinkButton.title = data.profileInsightsLinkTitle configureSidebar(data.sidebarItems) updateDocumentLayout() } private func setupLayout() { wantsLayer = true layer?.backgroundColor = Theme.pageBackground.cgColor scrollView.translatesAutoresizingMaskIntoConstraints = false scrollView.hasVerticalScroller = true scrollView.drawsBackground = false addSubview(scrollView) contentStack.orientation = .horizontal contentStack.spacing = 20 contentStack.translatesAutoresizingMaskIntoConstraints = false contentStack.alignment = .top contentStack.edgeInsets = NSEdgeInsets(top: 24, left: 24, bottom: 24, right: 24) documentContainer.translatesAutoresizingMaskIntoConstraints = true documentContainer.autoresizingMask = [.width] documentContainer.frame = NSRect(x: 0, y: 0, width: 1040, height: 900) chromeContainer.translatesAutoresizingMaskIntoConstraints = false chromeContainer.wantsLayer = true chromeContainer.layer?.backgroundColor = Theme.chromeBackground.cgColor chromeContainer.layer?.cornerRadius = 18 documentContainer.addSubview(chromeContainer) chromeContainer.addSubview(contentStack) scrollView.documentView = documentContainer sidebar.orientation = .vertical sidebar.spacing = 10 sidebar.distribution = .fill sidebar.alignment = .leading sidebar.wantsLayer = true sidebar.layer?.backgroundColor = Theme.sidebarBackground.cgColor sidebar.layer?.cornerRadius = 16 sidebar.edgeInsets = NSEdgeInsets(top: 18, left: 14, bottom: 18, right: 14) sidebar.translatesAutoresizingMaskIntoConstraints = false mainHost.translatesAutoresizingMaskIntoConstraints = false mainGradient.translatesAutoresizingMaskIntoConstraints = false mainGradient.wantsLayer = true mainGradient.layer?.cornerRadius = 16 mainGradient.layer?.masksToBounds = true mainHost.addSubview(mainGradient) mainHost.addSubview(mainOverlay) mainOverlay.orientation = .vertical mainOverlay.spacing = 0 mainOverlay.alignment = .centerX mainOverlay.translatesAutoresizingMaskIntoConstraints = false mainOverlay.setContentHuggingPriority(.required, for: .vertical) greetingLabel.font = .systemFont(ofSize: 32, weight: .bold) greetingLabel.textColor = Theme.primaryText greetingLabel.alignment = .center greetingLabel.maximumNumberOfLines = 1 subtitleLabel.font = .systemFont(ofSize: 15, weight: .regular) subtitleLabel.textColor = Theme.secondaryText subtitleLabel.alignment = .center subtitleLabel.maximumNumberOfLines = 2 let topInset = NSView() topInset.translatesAutoresizingMaskIntoConstraints = false topInset.heightAnchor.constraint(equalToConstant: 32).isActive = true configureInsightsCard() let titleBlock = NSStackView(views: [greetingLabel, subtitleLabel]) titleBlock.orientation = .vertical titleBlock.spacing = 10 titleBlock.alignment = .centerX let headerStack = NSStackView(views: [titleBlock, insightsLinkButton]) headerStack.orientation = .vertical headerStack.spacing = 18 headerStack.alignment = .centerX let midSpacer = NSView() midSpacer.translatesAutoresizingMaskIntoConstraints = false midSpacer.heightAnchor.constraint(equalToConstant: 20).isActive = true mainOverlay.addArrangedSubview(topInset) mainOverlay.addArrangedSubview(headerStack) mainOverlay.addArrangedSubview(midSpacer) mainOverlay.addArrangedSubview(insightsCard) sparkleView.translatesAutoresizingMaskIntoConstraints = false sparkleView.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 22, weight: .regular) sparkleView.image = NSImage(systemSymbolName: "sparkle", accessibilityDescription: "Accent") sparkleView.contentTintColor = NSColor(calibratedWhite: 0.9, alpha: 0.55) mainHost.addSubview(sparkleView) contentStack.addArrangedSubview(sidebar) contentStack.addArrangedSubview(mainHost) NSLayoutConstraint.activate([ scrollView.leadingAnchor.constraint(equalTo: leadingAnchor), scrollView.trailingAnchor.constraint(equalTo: trailingAnchor), scrollView.topAnchor.constraint(equalTo: topAnchor), scrollView.bottomAnchor.constraint(equalTo: bottomAnchor), chromeContainer.topAnchor.constraint(equalTo: documentContainer.topAnchor, constant: 18), chromeContainer.bottomAnchor.constraint(equalTo: documentContainer.bottomAnchor, constant: -18), chromeContainer.centerXAnchor.constraint(equalTo: documentContainer.centerXAnchor), chromeContainer.widthAnchor.constraint(lessThanOrEqualToConstant: 1240), chromeContainer.widthAnchor.constraint(equalTo: documentContainer.widthAnchor, constant: -36), contentStack.leadingAnchor.constraint(equalTo: chromeContainer.leadingAnchor), contentStack.trailingAnchor.constraint(equalTo: chromeContainer.trailingAnchor), contentStack.topAnchor.constraint(equalTo: chromeContainer.topAnchor), contentStack.bottomAnchor.constraint(equalTo: chromeContainer.bottomAnchor), sidebar.widthAnchor.constraint(equalToConstant: 225), mainHost.widthAnchor.constraint(greaterThanOrEqualToConstant: 720), mainGradient.leadingAnchor.constraint(equalTo: mainHost.leadingAnchor), mainGradient.trailingAnchor.constraint(equalTo: mainHost.trailingAnchor), mainGradient.topAnchor.constraint(equalTo: mainHost.topAnchor), mainGradient.bottomAnchor.constraint(equalTo: mainHost.bottomAnchor), mainOverlay.leadingAnchor.constraint(equalTo: mainHost.leadingAnchor), mainOverlay.trailingAnchor.constraint(equalTo: mainHost.trailingAnchor), mainOverlay.topAnchor.constraint(equalTo: mainHost.topAnchor), mainHost.bottomAnchor.constraint(equalTo: mainOverlay.bottomAnchor, constant: 24), greetingLabel.leadingAnchor.constraint(equalTo: mainOverlay.leadingAnchor, constant: 24), greetingLabel.trailingAnchor.constraint(equalTo: mainOverlay.trailingAnchor, constant: -24), subtitleLabel.leadingAnchor.constraint(equalTo: greetingLabel.leadingAnchor), subtitleLabel.trailingAnchor.constraint(equalTo: greetingLabel.trailingAnchor), insightsLinkButton.leadingAnchor.constraint(equalTo: greetingLabel.leadingAnchor), insightsLinkButton.trailingAnchor.constraint(equalTo: greetingLabel.trailingAnchor), sparkleView.trailingAnchor.constraint(equalTo: mainHost.trailingAnchor, constant: -28), sparkleView.bottomAnchor.constraint(equalTo: mainHost.bottomAnchor, constant: -28) ]) } private func configureInsightsCard() { insightsCard.wantsLayer = true insightsCard.layer?.backgroundColor = Theme.cardBackground.cgColor insightsCard.layer?.cornerRadius = 18 insightsCard.translatesAutoresizingMaskIntoConstraints = false insightsTitleLabel.font = .systemFont(ofSize: 20, weight: .semibold) insightsTitleLabel.textColor = Theme.primaryText insightsTitleLabel.alignment = .center insightsTitleLabel.maximumNumberOfLines = 1 insightsBodyLabel.font = .systemFont(ofSize: 13, weight: .regular) insightsBodyLabel.textColor = Theme.secondaryText insightsBodyLabel.alignment = .center insightsBodyLabel.maximumNumberOfLines = 4 insightsBodyLabel.lineBreakMode = .byWordWrapping insightsBodyLabel.preferredMaxLayoutWidth = 400 insightsLinkButton.bezelStyle = .inline insightsLinkButton.isBordered = false insightsLinkButton.font = .systemFont(ofSize: 13, weight: .medium) insightsLinkButton.contentTintColor = Theme.linkText insightsLinkButton.setButtonType(.momentaryPushIn) togglesLabel.font = .systemFont(ofSize: 12, weight: .medium) togglesLabel.textColor = Theme.tertiaryText togglesLabel.alignment = .center togglesLabel.maximumNumberOfLines = 1 styleToggle(savedToggleButton) styleToggle(interviewsToggleButton) let toggleRow = NSStackView(views: [savedToggleButton, interviewsToggleButton]) toggleRow.orientation = .horizontal toggleRow.spacing = 10 toggleRow.alignment = .centerY let inner = NSStackView(views: [ insightsTitleLabel, insightsBodyLabel, togglesLabel, toggleRow ]) inner.orientation = .vertical inner.spacing = 14 inner.alignment = .centerX inner.translatesAutoresizingMaskIntoConstraints = false insightsCard.addSubview(inner) NSLayoutConstraint.activate([ inner.leadingAnchor.constraint(equalTo: insightsCard.leadingAnchor, constant: 32), inner.trailingAnchor.constraint(equalTo: insightsCard.trailingAnchor, constant: -32), inner.topAnchor.constraint(equalTo: insightsCard.topAnchor, constant: 28), inner.bottomAnchor.constraint(equalTo: insightsCard.bottomAnchor, constant: -28), insightsCard.widthAnchor.constraint(equalToConstant: 440) ]) } private func styleToggle(_ button: NSButton) { button.bezelStyle = .rounded button.font = .systemFont(ofSize: 12, weight: .medium) button.contentTintColor = Theme.secondaryText button.wantsLayer = true button.layer?.backgroundColor = Theme.toggleBackground.cgColor button.layer?.cornerRadius = 8 button.translatesAutoresizingMaskIntoConstraints = false button.widthAnchor.constraint(equalToConstant: 108).isActive = true button.heightAnchor.constraint(equalToConstant: 30).isActive = true } private func configureSidebar(_ items: [SidebarItem]) { sidebar.arrangedSubviews.forEach { sidebar.removeArrangedSubview($0) $0.removeFromSuperview() } let brand = NSTextField(labelWithString: "Indeed AI\nJob Finder") brand.font = .systemFont(ofSize: 18, weight: .bold) brand.textColor = Theme.primaryText brand.alignment = .left sidebar.addArrangedSubview(brand) let titleToMenuSpacer = NSView() titleToMenuSpacer.translatesAutoresizingMaskIntoConstraints = false titleToMenuSpacer.heightAnchor.constraint(equalToConstant: 24).isActive = true sidebar.addArrangedSubview(titleToMenuSpacer) items.enumerated().forEach { index, item in let row = NSStackView() row.orientation = .horizontal row.spacing = 8 row.alignment = .centerY row.wantsLayer = true row.layer?.cornerRadius = 8 row.edgeInsets = NSEdgeInsets(top: 8, left: 10, bottom: 8, right: 10) let isSelected = index == 0 if isSelected { row.layer?.backgroundColor = Theme.selectionFill.cgColor } let icon = NSImageView() icon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 13, weight: .medium) icon.image = NSImage(systemSymbolName: item.systemImage, accessibilityDescription: item.title) icon.contentTintColor = isSelected ? Theme.primaryText : Theme.secondaryText let text = NSTextField(labelWithString: item.title) text.font = .systemFont(ofSize: 14, weight: .medium) text.textColor = isSelected ? Theme.primaryText : Theme.secondaryText row.addArrangedSubview(icon) row.addArrangedSubview(text) if let badge = item.badge { let badgeField = NSTextField(labelWithString: badge) badgeField.font = .systemFont(ofSize: 11, weight: .semibold) badgeField.textColor = Theme.primaryText badgeField.wantsLayer = true badgeField.layer?.backgroundColor = Theme.toggleBackground.cgColor badgeField.layer?.cornerRadius = 8 badgeField.alignment = .center badgeField.maximumNumberOfLines = 1 badgeField.lineBreakMode = .byClipping badgeField.translatesAutoresizingMaskIntoConstraints = false badgeField.widthAnchor.constraint(equalToConstant: 42).isActive = true row.addArrangedSubview(NSView()) row.addArrangedSubview(badgeField) } sidebar.addArrangedSubview(row) } let sidebarBottomSpacer = NSView() sidebarBottomSpacer.translatesAutoresizingMaskIntoConstraints = false sidebarBottomSpacer.setContentHuggingPriority(.defaultLow, for: .vertical) sidebarBottomSpacer.setContentCompressionResistancePriority(.defaultLow, for: .vertical) sidebar.addArrangedSubview(sidebarBottomSpacer) } private func updateDocumentLayout() { documentContainer.layoutSubtreeIfNeeded() let fittingHeight = max(chromeContainer.fittingSize.height + 36, bounds.height) documentContainer.frame = NSRect(x: 0, y: 0, width: bounds.width, height: fittingHeight) } } // MARK: - Neutral main-area gradient (black / grey only) private final class NeutralGradientBackgroundView: NSView { private let gradientLayer = CAGradientLayer() override init(frame frameRect: NSRect) { super.init(frame: frameRect) commonInit() } required init?(coder: NSCoder) { super.init(coder: coder) commonInit() } private func commonInit() { wantsLayer = true gradientLayer.colors = [ NSColor(calibratedWhite: 0.12, alpha: 1).cgColor, NSColor(calibratedWhite: 0.05, alpha: 1).cgColor, NSColor.black.cgColor ] gradientLayer.locations = [0, 0.42, 1] as [NSNumber] gradientLayer.startPoint = CGPoint(x: 0.5, y: 1) gradientLayer.endPoint = CGPoint(x: 0.5, y: 0) layer?.addSublayer(gradientLayer) } override func layout() { super.layout() gradientLayer.frame = bounds } }