import Cocoa // MARK: - Content Panel final class ContentPanelView: NSView, AppearanceRefreshable { init(cornerRadius: CGFloat = AppTheme.contentPanelCornerRadius) { super.init(frame: .zero) wantsLayer = true layer?.cornerRadius = cornerRadius layer?.borderWidth = 1.5 applyCardShadow() refreshAppearance() } @available(*, unavailable) required init?(coder: NSCoder) { nil } func refreshAppearance() { layer?.backgroundColor = AppTheme.cardBackground.cgColor layer?.borderColor = AppTheme.paywallBorder.cgColor } } // MARK: - Gradient Card View final class GradientCardView: NSView { private let gradientLayer = CAGradientLayer() init(colors: [NSColor], startPoint: CGPoint = CGPoint(x: 0, y: 0.5), endPoint: CGPoint = CGPoint(x: 1, y: 0.5)) { super.init(frame: .zero) wantsLayer = true gradientLayer.colors = colors.map { $0.cgColor } gradientLayer.startPoint = startPoint gradientLayer.endPoint = endPoint gradientLayer.cornerRadius = AppTheme.cardCornerRadius layer?.addSublayer(gradientLayer) applyCardShadow() } @available(*, unavailable) required init?(coder: NSCoder) { nil } override func layout() { super.layout() gradientLayer.frame = bounds gradientLayer.cornerRadius = AppTheme.cardCornerRadius } } // MARK: - Wave Pattern final class WavePatternView: NSView, AppearanceRefreshable { var fixedDotColor: NSColor? override var isOpaque: Bool { false } func refreshAppearance() { guard fixedDotColor == nil else { return } needsDisplay = true } override func draw(_ dirtyRect: NSRect) { super.draw(dirtyRect) guard let context = NSGraphicsContext.current?.cgContext else { return } context.setFillColor((fixedDotColor ?? AppTheme.waveDotColor).cgColor) let spacing: CGFloat = 14 let dotSize: CGFloat = 3 let rows = Int(bounds.height / spacing) + 2 let cols = Int(bounds.width / spacing) + 2 for row in 0.. Void)? private let accent: Accent private let iconView = NSImageView() private let titleLabel = NSTextField(labelWithString: "") private let container = NSView() private var hoverTracker: HoverTracker? private var isHovered = false var isSelected: Bool = false { didSet { updateAppearance() } } init(title: String, symbolName: String, isSelected: Bool = false, accent: Accent = .standard) { self.accent = accent super.init(frame: .zero) self.isSelected = isSelected titleLabel.stringValue = title titleLabel.font = AppTheme.mediumFont(size: 14) titleLabel.themeLabelStyle = .primary titleLabel.textColor = AppTheme.textPrimary if let image = NSImage(systemSymbolName: symbolName, accessibilityDescription: title) { let config = NSImage.SymbolConfiguration(pointSize: 15, weight: .medium) iconView.image = image.withSymbolConfiguration(config) } iconView.contentTintColor = AppTheme.textPrimary container.translatesAutoresizingMaskIntoConstraints = false iconView.translatesAutoresizingMaskIntoConstraints = false titleLabel.translatesAutoresizingMaskIntoConstraints = false translatesAutoresizingMaskIntoConstraints = false addSubview(container) container.addSubview(iconView) container.addSubview(titleLabel) NSLayoutConstraint.activate([ heightAnchor.constraint(equalToConstant: 44), container.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12), container.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12), container.topAnchor.constraint(equalTo: topAnchor), container.bottomAnchor.constraint(equalTo: bottomAnchor), iconView.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 12), iconView.centerYAnchor.constraint(equalTo: container.centerYAnchor), iconView.widthAnchor.constraint(equalToConstant: 20), iconView.heightAnchor.constraint(equalToConstant: 20), titleLabel.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 10), titleLabel.centerYAnchor.constraint(equalTo: container.centerYAnchor), titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: container.trailingAnchor, constant: -12), ]) updateAppearance() hoverTracker = HoverTracker(view: self) { [weak self] hovering in self?.isHovered = hovering self?.updateAppearance() } } @available(*, unavailable) required init?(coder: NSCoder) { nil } func refreshAppearance() { updateAppearance() } private func updateAppearance() { if isSelected { applyStyle(.active) } else if isHovered { applyStyle(.hovered) } else { applyStyle(.normal) } } private func applyStyle(_ style: Style) { container.wantsLayer = true container.layer?.cornerRadius = AppTheme.cornerRadius let activeBackground = accent == .premium ? AppTheme.premiumBackground : AppTheme.homeActiveBackground let activeForeground = accent == .premium ? AppTheme.premiumForeground : AppTheme.homeActiveForeground let hoverBackground = accent == .premium ? AppTheme.premiumHoverBackground : AppTheme.sidebarHoverBackground let normalForeground = accent == .premium ? AppTheme.premiumForeground : AppTheme.textPrimary animateHover { switch style { case .normal: container.layer?.backgroundColor = NSColor.clear.cgColor titleLabel.textColor = normalForeground iconView.contentTintColor = normalForeground case .hovered: container.layer?.backgroundColor = hoverBackground.cgColor titleLabel.textColor = normalForeground iconView.contentTintColor = normalForeground case .active: container.layer?.backgroundColor = activeBackground.cgColor titleLabel.textColor = activeForeground iconView.contentTintColor = activeForeground } } } override func mouseUp(with event: NSEvent) { guard bounds.contains(convert(event.locationInWindow, from: nil)) else { return } onClick?() } override func resetCursorRects() { addCursorRect(bounds, cursor: .pointingHand) } } // MARK: - Action Button final class PillButton: NSButton { private let horizontalPadding: CGFloat = 16 private let baseColor: NSColor private var hoverTracker: HoverTracker? private let clickTarget: ButtonClickTarget var onClick: (() -> Void)? { get { clickTarget.handler } set { clickTarget.handler = newValue } } init(title: String, color: NSColor) { clickTarget = ButtonClickTarget() baseColor = color super.init(frame: .zero) self.title = title isBordered = false wantsLayer = true layer?.backgroundColor = color.cgColor layer?.cornerRadius = 11 font = AppTheme.semiboldFont(size: 13) contentTintColor = .white target = clickTarget action = #selector(ButtonClickTarget.activate) if let arrow = NSImage(systemSymbolName: "arrow.right", accessibilityDescription: nil) { let config = NSImage.SymbolConfiguration(pointSize: 11, weight: .bold) image = arrow.withSymbolConfiguration(config) imagePosition = .imageTrailing imageHugsTitle = true } heightAnchor.constraint(equalToConstant: 40).isActive = true hoverTracker = HoverTracker(view: self) { [weak self] hovering in self?.setHovered(hovering) } } private func setHovered(_ hovering: Bool) { let color = hovering ? baseColor.blended(withFraction: 0.15, of: .black) ?? baseColor : baseColor animateHover { layer?.backgroundColor = color.cgColor layer?.transform = hovering ? CATransform3DMakeScale(1.03, 1.03, 1) : CATransform3DIdentity } } @available(*, unavailable) required init?(coder: NSCoder) { nil } override var intrinsicContentSize: NSSize { var size = super.intrinsicContentSize size.width += horizontalPadding * 2 return size } override func draw(_ dirtyRect: NSRect) { let originalBounds = bounds defer { bounds = originalBounds } bounds = originalBounds.insetBy(dx: horizontalPadding, dy: 0) super.draw(dirtyRect) } override func resetCursorRects() { addCursorRect(bounds, cursor: .pointingHand) } } // MARK: - Sparkle Icon final class SparkleIconView: NSView { var color: NSColor = AppTheme.blue override var isFlipped: Bool { true } override var isOpaque: Bool { false } override func draw(_ dirtyRect: NSRect) { super.draw(dirtyRect) guard let context = NSGraphicsContext.current?.cgContext else { return } let s = min(bounds.width, bounds.height) let cx = bounds.midX let cy = bounds.midY let tipR = s * 0.34 let valleyR = s * 0.09 let tips = [ CGPoint(x: cx, y: cy - tipR), CGPoint(x: cx + tipR, y: cy), CGPoint(x: cx, y: cy + tipR), CGPoint(x: cx - tipR, y: cy), ] let valleys = [ CGPoint(x: cx + valleyR, y: cy - valleyR), CGPoint(x: cx + valleyR, y: cy + valleyR), CGPoint(x: cx - valleyR, y: cy + valleyR), CGPoint(x: cx - valleyR, y: cy - valleyR), ] let starPath = CGMutablePath() starPath.move(to: tips[0]) for i in 0..<4 { starPath.addQuadCurve(to: tips[(i + 1) % 4], control: valleys[i]) } starPath.closeSubpath() context.setFillColor(color.cgColor) context.addPath(starPath) context.fillPath() let dotR = s * 0.052 let dotOffset = s * 0.30 let dotPositions = [ CGPoint(x: cx + dotOffset, y: cy - dotOffset), CGPoint(x: cx + dotOffset, y: cy + dotOffset), CGPoint(x: cx - dotOffset, y: cy + dotOffset), CGPoint(x: cx - dotOffset, y: cy - dotOffset), ] for pos in dotPositions { context.fillEllipse(in: CGRect(x: pos.x - dotR, y: pos.y - dotR, width: dotR * 2, height: dotR * 2)) } } } // MARK: - Quick Start Card struct QuickStartCardData { let title: String let subtitle: String let buttonTitle: String let accentColor: NSColor let gradientColors: [NSColor] let iconKind: QuickStartIconKind } final class QuickStartCardView: NSView, AppearanceRefreshable { private let iconView: QuickStartIconView private let gradientView: GradientCardView private var iconWidthConstraint: NSLayoutConstraint! private var iconHeightConstraint: NSLayoutConstraint! private var hoverTracker: HoverTracker? private var isHovered = false private let onActivate: () -> Void init(data: QuickStartCardData, onActivate: @escaping () -> Void) { self.onActivate = onActivate iconView = QuickStartIconView(kind: data.iconKind) gradientView = GradientCardView( colors: data.gradientColors, startPoint: CGPoint(x: 0, y: 0.5), endPoint: CGPoint(x: 1, y: 0.5) ) super.init(frame: .zero) translatesAutoresizingMaskIntoConstraints = false let gradient = gradientView gradient.translatesAutoresizingMaskIntoConstraints = false let titleLabel = NSTextField(labelWithString: data.title) titleLabel.font = AppTheme.semiboldFont(size: 22) titleLabel.textColor = data.accentColor titleLabel.translatesAutoresizingMaskIntoConstraints = false let subtitleLabel = NSTextField(labelWithString: data.subtitle) subtitleLabel.font = AppTheme.regularFont(size: 13) subtitleLabel.textColor = AppTheme.quickStartCardSubtitle subtitleLabel.translatesAutoresizingMaskIntoConstraints = false let button = PillButton(title: data.buttonTitle, color: data.accentColor) button.translatesAutoresizingMaskIntoConstraints = false button.onClick = onActivate addSubview(gradient) gradient.addSubview(titleLabel) gradient.addSubview(subtitleLabel) gradient.addSubview(button) gradient.addSubview(iconView) NSLayoutConstraint.activate([ gradient.leadingAnchor.constraint(equalTo: leadingAnchor), gradient.trailingAnchor.constraint(equalTo: trailingAnchor), gradient.topAnchor.constraint(equalTo: topAnchor), gradient.bottomAnchor.constraint(equalTo: bottomAnchor), heightAnchor.constraint(equalToConstant: 148), titleLabel.leadingAnchor.constraint(equalTo: gradient.leadingAnchor, constant: 18), titleLabel.topAnchor.constraint(equalTo: gradient.topAnchor, constant: 24), subtitleLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 4), subtitleLabel.trailingAnchor.constraint(lessThanOrEqualTo: iconView.leadingAnchor, constant: -8), button.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), button.bottomAnchor.constraint(equalTo: gradient.bottomAnchor, constant: -20), iconView.trailingAnchor.constraint(equalTo: gradient.trailingAnchor, constant: -8), iconView.centerYAnchor.constraint(equalTo: gradient.centerYAnchor), ]) iconWidthConstraint = iconView.widthAnchor.constraint(equalToConstant: AppTheme.quickStartIconMax) iconHeightConstraint = iconView.heightAnchor.constraint(equalToConstant: AppTheme.quickStartIconMax) iconWidthConstraint.isActive = true iconHeightConstraint.isActive = true hoverTracker = HoverTracker(view: self) { [weak self] hovering in self?.setHovered(hovering) } refreshAppearance() } @available(*, unavailable) required init?(coder: NSCoder) { nil } func refreshAppearance() { gradientView.layer?.cornerRadius = AppTheme.cardCornerRadius gradientView.layer?.borderWidth = isHovered ? 2 : 1.5 gradientView.layer?.borderColor = AppTheme.paywallBorder.cgColor if isHovered { applyHoverLift(true, on: gradientView.layer) } } private func setHovered(_ hovering: Bool) { isHovered = hovering applyHoverLift(hovering, on: gradientView.layer) gradientView.layer?.borderWidth = hovering ? 2 : 1.5 } override func layout() { super.layout() let size = AppTheme.quickStartIconSize(forCardWidth: bounds.width) if iconWidthConstraint.constant != size { iconWidthConstraint.constant = size iconHeightConstraint.constant = size } } override func mouseUp(with event: NSEvent) { let location = convert(event.locationInWindow, from: nil) guard bounds.contains(location) else { return } onActivate() } override func resetCursorRects() { addCursorRect(bounds, cursor: .pointingHand) } } private final class ButtonClickTarget: NSObject { var handler: (() -> Void)? @objc func activate() { handler?() } } // MARK: - Feature Card struct FeatureCardData { let title: String let subtitle: String let iconKind: FeatureIconKind var onActivate: (() -> Void)? = nil } final class FeatureCardView: NSView, AppearanceRefreshable { private let iconView: FeatureIconView private let titleLabel: NSTextField private let subtitleLabel: NSTextField private let arrowButton: NSButton private var iconWidthConstraint: NSLayoutConstraint! private var iconHeightConstraint: NSLayoutConstraint! private var hoverTracker: HoverTracker? private var isHovered = false private let onActivate: (() -> Void)? init(data: FeatureCardData) { onActivate = data.onActivate iconView = FeatureIconView(kind: data.iconKind) titleLabel = NSTextField.themeLabel(data.title, style: .primary, font: AppTheme.semiboldFont(size: 14)) subtitleLabel = NSTextField.themeLabel(data.subtitle, style: .secondary, font: AppTheme.regularFont(size: 11)) arrowButton = NSButton() super.init(frame: .zero) translatesAutoresizingMaskIntoConstraints = false setContentHuggingPriority(.defaultLow, for: .horizontal) setContentCompressionResistancePriority(.defaultLow, for: .horizontal) wantsLayer = true layer?.cornerRadius = AppTheme.featureCardCornerRadius applyCardShadow() titleLabel.translatesAutoresizingMaskIntoConstraints = false subtitleLabel.translatesAutoresizingMaskIntoConstraints = false arrowButton.isBordered = false arrowButton.wantsLayer = true arrowButton.layer?.cornerRadius = 13 arrowButton.layer?.borderWidth = 1.5 arrowButton.translatesAutoresizingMaskIntoConstraints = false if let arrow = NSImage(systemSymbolName: "chevron.right", accessibilityDescription: "Open") { let config = NSImage.SymbolConfiguration(pointSize: 10, weight: .semibold) arrowButton.image = arrow.withSymbolConfiguration(config) } arrowButton.contentTintColor = AppTheme.textSecondary addSubview(iconView) addSubview(titleLabel) addSubview(subtitleLabel) addSubview(arrowButton) NSLayoutConstraint.activate([ heightAnchor.constraint(equalToConstant: 96), iconView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12), iconView.centerYAnchor.constraint(equalTo: centerYAnchor), titleLabel.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 10), titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 20), titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -32), subtitleLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 3), subtitleLabel.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor), arrowButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -10), arrowButton.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -10), arrowButton.widthAnchor.constraint(equalToConstant: 26), arrowButton.heightAnchor.constraint(equalToConstant: 26), ]) iconWidthConstraint = iconView.widthAnchor.constraint(equalToConstant: AppTheme.featureIconMax) iconHeightConstraint = iconView.heightAnchor.constraint(equalToConstant: AppTheme.featureIconMax) iconWidthConstraint.isActive = true iconHeightConstraint.isActive = true refreshAppearance() hoverTracker = HoverTracker(view: self) { [weak self] hovering in self?.setHovered(hovering) } } @available(*, unavailable) required init?(coder: NSCoder) { nil } private func setHovered(_ hovering: Bool) { isHovered = hovering applyHoverLift(hovering) let borderWidth: CGFloat = hovering ? 2 : 1.5 layer?.borderWidth = borderWidth arrowButton.layer?.borderWidth = borderWidth } func refreshAppearance() { layer?.backgroundColor = AppTheme.cardBackground.cgColor layer?.borderWidth = isHovered ? 2 : 1.5 layer?.borderColor = AppTheme.paywallBorder.cgColor titleLabel.refreshThemeLabelColor() subtitleLabel.refreshThemeLabelColor() arrowButton.layer?.backgroundColor = AppTheme.elevatedBackground.cgColor arrowButton.layer?.borderWidth = isHovered ? 2 : 1.5 arrowButton.layer?.borderColor = AppTheme.paywallBorder.cgColor arrowButton.contentTintColor = AppTheme.textSecondary if isHovered { applyHoverLift(true) } } override func layout() { super.layout() let size = AppTheme.featureIconSize(forCardWidth: bounds.width) if iconWidthConstraint.constant != size { iconWidthConstraint.constant = size iconHeightConstraint.constant = size } } override func mouseUp(with event: NSEvent) { let location = convert(event.locationInWindow, from: nil) guard bounds.contains(location) else { return } onActivate?() } override func resetCursorRects() { guard onActivate != nil else { return } addCursorRect(bounds, cursor: .pointingHand) } }