| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410 |
- import Cocoa
- import ObjectiveC
- enum AppTheme {
- static let windowWidth: CGFloat = 800
- static let windowHeight: CGFloat = 720
- static let windowMinWidth: CGFloat = 690
- static let windowMinHeight: CGFloat = windowHeight
- static let sidebarWidth: CGFloat = 240
- static let contentPadding: CGFloat = 20
- static let quickStartSpacing: CGFloat = 12
- static let featureGridSpacing: CGFloat = 10
- static let featureGridColumns = 3
- static let quickStartIconMax: CGFloat = 80
- static let quickStartIconMin: CGFloat = 56
- static let featureIconMax: CGFloat = 48
- static let featureIconMin: CGFloat = 36
- static func quickStartIconSize(forCardWidth width: CGFloat) -> CGFloat {
- min(quickStartIconMax, max(quickStartIconMin, width * 0.58))
- }
- static func featureIconSize(forCardWidth width: CGFloat) -> CGFloat {
- min(featureIconMax, max(featureIconMin, width * 0.24))
- }
- static let cornerRadius: CGFloat = 14
- static let cardCornerRadius: CGFloat = 20
- static let featureCardCornerRadius: CGFloat = 16
- static let contentPanelCornerRadius: CGFloat = 24
- static let contentPanelInset: CGFloat = 16
- static let headerTopInset: CGFloat = 14
- static let headerHeight: CGFloat = 32
- static let contentBelowHeaderGap: CGFloat = 2
- private static var isDark: Bool { AppSettings.darkModeEnabled }
- static var background: NSColor {
- isDark
- ? NSColor(calibratedWhite: 0.11, alpha: 1)
- : NSColor(calibratedWhite: 0.965, alpha: 1)
- }
- static var sidebarBackground: NSColor {
- isDark
- ? NSColor(calibratedWhite: 0.15, alpha: 1)
- : NSColor.white
- }
- static var cardBackground: NSColor {
- isDark
- ? NSColor(calibratedWhite: 0.18, alpha: 1)
- : NSColor.white
- }
- static var groupCardBackground: NSColor { cardBackground }
- static var elevatedBackground: NSColor {
- isDark
- ? NSColor(calibratedWhite: 0.22, alpha: 1)
- : NSColor.white
- }
- static var textPrimary: NSColor {
- isDark
- ? NSColor(calibratedWhite: 0.92, alpha: 1)
- : NSColor(calibratedWhite: 0.12, alpha: 1)
- }
- static var textSecondary: NSColor {
- isDark
- ? NSColor(calibratedWhite: 0.55, alpha: 1)
- : NSColor(calibratedWhite: 0.48, alpha: 1)
- }
- static var border: NSColor {
- isDark
- ? NSColor(calibratedWhite: 0.28, alpha: 1)
- : NSColor(calibratedWhite: 0.92, alpha: 1)
- }
- static var waveDotColor: NSColor {
- isDark
- ? NSColor(calibratedWhite: 0.35, alpha: 0.45)
- : NSColor(calibratedWhite: 0.82, alpha: 0.55)
- }
- static var homeActiveBackground: NSColor {
- isDark
- ? NSColor(red: 0.18, green: 0.28, blue: 0.48, alpha: 1)
- : NSColor(red: 0.91, green: 0.94, blue: 1.0, alpha: 1)
- }
- static var homeActiveForeground: NSColor {
- isDark
- ? NSColor(red: 0.45, green: 0.65, blue: 1.0, alpha: 1)
- : NSColor(red: 0.22, green: 0.47, blue: 0.96, alpha: 1)
- }
- static var sidebarHoverBackground: NSColor {
- isDark
- ? NSColor(calibratedWhite: 0.20, alpha: 1)
- : NSColor(calibratedWhite: 0.96, alpha: 1)
- }
- static var premiumHoverBackground: NSColor {
- isDark
- ? NSColor(red: 0.24, green: 0.20, blue: 0.36, alpha: 1)
- : NSColor(red: 0.97, green: 0.95, blue: 1.0, alpha: 1)
- }
- static let hoverAnimationDuration: TimeInterval = 0.15
- static var cardShadowOpacity: Float {
- AppSettings.darkModeEnabled ? 0.25 : 0.07
- }
- static var cardHoverShadowOpacity: Float {
- AppSettings.darkModeEnabled ? 0.35 : 0.14
- }
- static let cardShadowRadius: CGFloat = 14
- static let cardHoverShadowRadius: CGFloat = 18
- static let cardHoverScale: CGFloat = 1.02
- static var premiumBackground: NSColor {
- isDark
- ? NSColor(red: 0.24, green: 0.20, blue: 0.36, alpha: 1)
- : NSColor(red: 0.95, green: 0.93, blue: 1.0, alpha: 1)
- }
- static var premiumCardBackground: NSColor {
- isDark ? cardBackground : NSColor(red: 0.95, green: 0.93, blue: 1.0, alpha: 1)
- }
- static var premiumForeground: NSColor {
- isDark
- ? NSColor(red: 0.72, green: 0.58, blue: 1.0, alpha: 1)
- : NSColor(red: 0.55, green: 0.36, blue: 0.96, alpha: 1)
- }
- static var premiumCardBorder: NSColor {
- isDark ? paywallBorder : NSColor(red: 0.88, green: 0.82, blue: 0.98, alpha: 1)
- }
- static var premiumCardTitle: NSColor { textPrimary }
- static var premiumCardSubtitle: NSColor {
- isDark ? textSecondary : NSColor(red: 0.44, green: 0.38, blue: 0.58, alpha: 1)
- }
- static let premiumCrown = NSColor(red: 0.95, green: 0.75, blue: 0.20, alpha: 1)
- static var premiumActionBackground: NSColor {
- isDark ? elevatedBackground : cardBackground
- }
- static var premiumActionHoverBackground: NSColor {
- isDark ? paywallTrustIconBackground : NSColor(red: 0.98, green: 0.96, blue: 1.0, alpha: 1)
- }
- static var premiumActionBorder: NSColor { paywallBorder }
- static var premiumActionBorderHover: NSColor { paywallBorder }
- static var premiumActionForeground: NSColor {
- isDark ? textPrimary : premiumForeground
- }
- static var premiumActionForegroundHover: NSColor {
- isDark ? premiumForeground : NSColor(red: 0.48, green: 0.28, blue: 0.90, alpha: 1)
- }
- static var premiumButtonBackground: NSColor { purple }
- static var premiumButtonHoverBackground: NSColor {
- isDark
- ? NSColor(red: 0.62, green: 0.45, blue: 1.0, alpha: 1)
- : NSColor(red: 0.48, green: 0.28, blue: 0.90, alpha: 1)
- }
- static let premiumButtonForeground = NSColor.white
- static let blue = NSColor(red: 0.22, green: 0.47, blue: 0.96, alpha: 1)
- static let quickStartBlueLight = NSColor(red: 0.88, green: 0.93, blue: 1.0, alpha: 1)
- static let quickStartCardSubtitle = NSColor(calibratedWhite: 0.48, alpha: 1)
- static var blueLight: NSColor {
- isDark
- ? NSColor(red: 0.20, green: 0.28, blue: 0.42, alpha: 1)
- : quickStartBlueLight
- }
- static let green = NSColor(red: 0.13, green: 0.68, blue: 0.42, alpha: 1)
- static let greenLight = NSColor(red: 0.88, green: 0.97, blue: 0.91, alpha: 1)
- static let orange = NSColor(red: 0.96, green: 0.52, blue: 0.18, alpha: 1)
- static let orangeLight = NSColor(red: 1.0, green: 0.94, blue: 0.88, alpha: 1)
- static let purple = NSColor(red: 0.55, green: 0.36, blue: 0.96, alpha: 1)
- static let teal = NSColor(red: 0.18, green: 0.72, blue: 0.82, alpha: 1)
- static let navy = NSColor(red: 0.12, green: 0.18, blue: 0.32, alpha: 1)
- static let paywallPink = NSColor(red: 0.98, green: 0.85, blue: 0.90, alpha: 1)
- static let paywallPinkText = NSColor(red: 0.75, green: 0.30, blue: 0.45, alpha: 1)
- static let paywallGold = NSColor(red: 0.96, green: 0.90, blue: 0.78, alpha: 1)
- static let paywallGoldText = NSColor(red: 0.65, green: 0.48, blue: 0.22, alpha: 1)
- static var paywallBorder: NSColor {
- isDark
- ? NSColor(calibratedWhite: 0.36, alpha: 1)
- : NSColor(calibratedWhite: 0.80, alpha: 1)
- }
- static var paywallBackground: NSColor { cardBackground }
- static var paywallAccent: NSColor {
- isDark ? textPrimary : navy
- }
- static var paywallLeftGradientColors: [NSColor] {
- isDark
- ? [
- NSColor(calibratedWhite: 0.17, alpha: 1),
- NSColor(calibratedWhite: 0.13, alpha: 1),
- ]
- : [
- NSColor(red: 0.88, green: 0.94, blue: 1.0, alpha: 1),
- NSColor(red: 0.95, green: 0.97, blue: 1.0, alpha: 1),
- ]
- }
- static var paywallCTABackground: NSColor {
- isDark
- ? NSColor(calibratedWhite: 0.96, alpha: 1)
- : navy
- }
- static var paywallCTAForeground: NSColor {
- isDark
- ? NSColor(calibratedWhite: 0.10, alpha: 1)
- : NSColor.white
- }
- static var paywallTrustIconBackground: NSColor {
- isDark
- ? NSColor(calibratedWhite: 0.24, alpha: 1)
- : quickStartBlueLight
- }
- static var paywallIconAccent: NSColor {
- isDark
- ? NSColor(calibratedWhite: 0.78, alpha: 1)
- : navy
- }
- static var paywallTrustBackground: NSColor { elevatedBackground }
- static var paywallOverlayBackdrop: NSColor {
- isDark
- ? NSColor(calibratedWhite: 0.05, alpha: 0.55)
- : NSColor(calibratedWhite: 0.15, alpha: 0.22)
- }
- static func semiboldFont(size: CGFloat) -> NSFont {
- .systemFont(ofSize: size, weight: .semibold)
- }
- static func mediumFont(size: CGFloat) -> NSFont {
- .systemFont(ofSize: size, weight: .medium)
- }
- static func regularFont(size: CGFloat) -> NSFont {
- .systemFont(ofSize: size, weight: .regular)
- }
- }
- protocol AppearanceRefreshable: AnyObject {
- func refreshAppearance()
- }
- // MARK: - Hover Tracking
- final class HoverTracker: NSResponder {
- private weak var view: NSView?
- private let onHoverChanged: (Bool) -> Void
- init(view: NSView, onHoverChanged: @escaping (Bool) -> Void) {
- self.view = view
- self.onHoverChanged = onHoverChanged
- super.init()
- let area = NSTrackingArea(
- rect: .zero,
- options: [.mouseEnteredAndExited, .activeInKeyWindow, .inVisibleRect],
- owner: self,
- userInfo: nil
- )
- view.addTrackingArea(area)
- }
- @available(*, unavailable)
- required init?(coder: NSCoder) { nil }
- override func mouseEntered(with event: NSEvent) {
- onHoverChanged(true)
- }
- override func mouseExited(with event: NSEvent) {
- onHoverChanged(false)
- }
- }
- extension NSView {
- func animateHover(_ changes: () -> Void) {
- NSAnimationContext.runAnimationGroup { context in
- context.duration = AppTheme.hoverAnimationDuration
- context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
- context.allowsImplicitAnimation = true
- changes()
- }
- }
- func applyHoverLift(_ hovering: Bool, on targetLayer: CALayer? = nil) {
- let layer = targetLayer ?? layer
- animateHover {
- guard let layer else { return }
- layer.transform = hovering
- ? CATransform3DMakeScale(AppTheme.cardHoverScale, AppTheme.cardHoverScale, 1)
- : CATransform3DIdentity
- layer.shadowOpacity = hovering ? AppTheme.cardHoverShadowOpacity : AppTheme.cardShadowOpacity
- layer.shadowRadius = hovering ? AppTheme.cardHoverShadowRadius : AppTheme.cardShadowRadius
- }
- }
- func applyCardShadow() {
- wantsLayer = true
- layer?.shadowColor = NSColor.black.cgColor
- layer?.shadowOpacity = AppTheme.cardShadowOpacity
- layer?.shadowOffset = NSSize(width: 0, height: -3)
- layer?.shadowRadius = AppTheme.cardShadowRadius
- layer?.masksToBounds = false
- }
- func roundCorners(_ radius: CGFloat) {
- wantsLayer = true
- layer?.cornerRadius = radius
- layer?.masksToBounds = true
- }
- func refreshAppearanceRecursively() {
- if let refreshable = self as? AppearanceRefreshable {
- refreshable.refreshAppearance()
- }
- if let label = self as? NSTextField {
- label.refreshThemeLabelColor()
- }
- if let imageView = self as? NSImageView, imageView.usesThemeTint {
- imageView.contentTintColor = AppTheme.textPrimary
- }
- subviews.forEach { $0.refreshAppearanceRecursively() }
- }
- }
- private enum ThemeAssociatedKeys {
- static var labelStyle = 0
- static var usesThemeTint = 0
- }
- enum ThemeLabelStyle: String {
- case primary
- case secondary
- }
- extension NSTextField {
- static func themeLabel(_ string: String, style: ThemeLabelStyle, font: NSFont) -> NSTextField {
- let label = NSTextField(labelWithString: string)
- label.font = font
- label.themeLabelStyle = style
- label.textColor = style == .primary ? AppTheme.textPrimary : AppTheme.textSecondary
- return label
- }
- var themeLabelStyle: ThemeLabelStyle? {
- get {
- guard let rawValue = objc_getAssociatedObject(self, &ThemeAssociatedKeys.labelStyle) as? String else {
- return nil
- }
- return ThemeLabelStyle(rawValue: rawValue)
- }
- set {
- objc_setAssociatedObject(
- self,
- &ThemeAssociatedKeys.labelStyle,
- newValue?.rawValue,
- .OBJC_ASSOCIATION_RETAIN_NONATOMIC
- )
- }
- }
- func refreshThemeLabelColor() {
- guard let style = themeLabelStyle else { return }
- textColor = style == .primary ? AppTheme.textPrimary : AppTheme.textSecondary
- }
- }
- extension NSImageView {
- var usesThemeTint: Bool {
- get { (objc_getAssociatedObject(self, &ThemeAssociatedKeys.usesThemeTint) as? Bool) ?? false }
- set { objc_setAssociatedObject(self, &ThemeAssociatedKeys.usesThemeTint, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
- }
- }
|