| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462 |
- import Cocoa
- // 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..<rows {
- for col in 0..<cols {
- let wave = sin(Double(col) * 0.35 + Double(row) * 0.25) * 8
- let x = CGFloat(col) * spacing + CGFloat(wave)
- let y = CGFloat(row) * spacing
- let rect = CGRect(x: x, y: y, width: dotSize, height: dotSize)
- context.fillEllipse(in: rect)
- }
- }
- }
- }
- // MARK: - Sidebar Nav Item
- final class SidebarNavItem: NSControl, AppearanceRefreshable {
- enum Style {
- case normal
- case active
- }
- enum Accent {
- case standard
- case premium
- }
- var onClick: (() -> Void)?
- private let accent: Accent
- private let iconView = NSImageView()
- private let titleLabel = NSTextField(labelWithString: "")
- private let container = NSView()
- var isSelected: Bool = false {
- didSet { applyStyle(isSelected ? .active : .normal) }
- }
- 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),
- ])
- applyStyle(isSelected ? .active : .normal)
- }
- @available(*, unavailable)
- required init?(coder: NSCoder) { nil }
- func refreshAppearance() {
- applyStyle(isSelected ? .active : .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 normalForeground = accent == .premium ? AppTheme.premiumForeground : AppTheme.textPrimary
- switch style {
- case .normal:
- container.layer?.backgroundColor = NSColor.clear.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
- init(title: String, color: NSColor) {
- 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
- 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
- }
- @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 {
- private let iconView: QuickStartIconView
- private var iconWidthConstraint: NSLayoutConstraint!
- private var iconHeightConstraint: NSLayoutConstraint!
- init(data: QuickStartCardData) {
- iconView = QuickStartIconView(kind: data.iconKind)
- super.init(frame: .zero)
- translatesAutoresizingMaskIntoConstraints = false
- let gradient = GradientCardView(
- colors: data.gradientColors,
- startPoint: CGPoint(x: 0, y: 0.5),
- endPoint: CGPoint(x: 1, y: 0.5)
- )
- 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
- 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
- }
- @available(*, unavailable)
- required init?(coder: NSCoder) { nil }
- override func layout() {
- super.layout()
- let size = AppTheme.quickStartIconSize(forCardWidth: bounds.width)
- if iconWidthConstraint.constant != size {
- iconWidthConstraint.constant = size
- iconHeightConstraint.constant = size
- }
- }
- }
- // MARK: - Feature Card
- struct FeatureCardData {
- let title: String
- let subtitle: String
- let iconKind: FeatureIconKind
- }
- 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!
- init(data: FeatureCardData) {
- 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
- 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()
- }
- @available(*, unavailable)
- required init?(coder: NSCoder) { nil }
- func refreshAppearance() {
- layer?.backgroundColor = AppTheme.cardBackground.cgColor
- titleLabel.refreshThemeLabelColor()
- subtitleLabel.refreshThemeLabelColor()
- arrowButton.layer?.backgroundColor = AppTheme.elevatedBackground.cgColor
- arrowButton.layer?.borderColor = AppTheme.border.cgColor
- arrowButton.contentTintColor = AppTheme.textSecondary
- }
- override func layout() {
- super.layout()
- let size = AppTheme.featureIconSize(forCardWidth: bounds.width)
- if iconWidthConstraint.constant != size {
- iconWidthConstraint.constant = size
- iconHeightConstraint.constant = size
- }
- }
- override func resetCursorRects() {
- addCursorRect(bounds, cursor: .pointingHand)
- }
- }
|