Procházet zdrojové kódy

Apply dark mode across the app on launch and when toggled.

Add dynamic theme colors, live appearance refresh, premium page dark styling, and preserve Quick Start card colors in both modes.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 před 1 měsícem
rodič
revize
0ca58b3cd0

+ 4 - 1
smart_printer/AppDelegate.swift

@@ -10,8 +10,11 @@ class AppDelegate: NSObject, NSApplicationDelegate {
 
 
     private weak var mainWindowController: MainWindowController?
     private weak var mainWindowController: MainWindowController?
 
 
-    func applicationDidFinishLaunching(_ notification: Notification) {
+    func applicationWillFinishLaunching(_ notification: Notification) {
         AppSettings.applyAppearance()
         AppSettings.applyAppearance()
+    }
+
+    func applicationDidFinishLaunching(_ notification: Notification) {
         resolveMainWindowController()
         resolveMainWindowController()
         configureMainWindow()
         configureMainWindow()
         configurePreferencesMenu()
         configurePreferencesMenu()

+ 5 - 0
smart_printer/AppSettings.swift

@@ -148,6 +148,7 @@ enum AppSettings {
 
 
     static func applyAppearance() {
     static func applyAppearance() {
         NSApp.appearance = darkModeEnabled ? NSAppearance(named: .darkAqua) : NSAppearance(named: .aqua)
         NSApp.appearance = darkModeEnabled ? NSAppearance(named: .darkAqua) : NSAppearance(named: .aqua)
+        NotificationCenter.default.post(name: .appearanceDidChange, object: nil)
     }
     }
 
 
     static var appLanguage: String {
     static var appLanguage: String {
@@ -155,3 +156,7 @@ enum AppSettings {
         set { defaults.set(newValue, forKey: Key.appLanguage) }
         set { defaults.set(newValue, forKey: Key.appLanguage) }
     }
     }
 }
 }
+
+extension Notification.Name {
+    static let appearanceDidChange = Notification.Name("appearanceDidChange")
+}

+ 204 - 12
smart_printer/AppTheme.swift

@@ -1,4 +1,5 @@
 import Cocoa
 import Cocoa
+import ObjectiveC
 
 
 enum AppTheme {
 enum AppTheme {
     static let windowWidth: CGFloat = 760
     static let windowWidth: CGFloat = 760
@@ -29,19 +30,93 @@ enum AppTheme {
     static let cardCornerRadius: CGFloat = 20
     static let cardCornerRadius: CGFloat = 20
     static let featureCardCornerRadius: CGFloat = 16
     static let featureCardCornerRadius: CGFloat = 16
 
 
-    static let background = NSColor(calibratedWhite: 0.965, alpha: 1)
-    static let sidebarBackground = NSColor.white
-    static let cardBackground = NSColor.white
-    static let textPrimary = NSColor(calibratedWhite: 0.12, alpha: 1)
-    static let textSecondary = NSColor(calibratedWhite: 0.48, alpha: 1)
+    private static var isDark: Bool { AppSettings.darkModeEnabled }
 
 
-    static let homeActiveBackground = NSColor(red: 0.91, green: 0.94, blue: 1.0, alpha: 1)
-    static let homeActiveForeground = NSColor(red: 0.22, green: 0.47, blue: 0.96, alpha: 1)
-    static let premiumBackground = NSColor(red: 0.95, green: 0.93, blue: 1.0, alpha: 1)
-    static let premiumForeground = NSColor(red: 0.55, green: 0.36, blue: 0.96, alpha: 1)
+    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 premiumBackground: NSColor {
+        isDark
+            ? NSColor(red: 0.28, green: 0.22, blue: 0.42, alpha: 1)
+            : 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 let blue = NSColor(red: 0.22, green: 0.47, blue: 0.96, alpha: 1)
     static let blue = NSColor(red: 0.22, green: 0.47, blue: 0.96, alpha: 1)
-    static let blueLight = NSColor(red: 0.88, green: 0.93, blue: 1.0, 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 let quickStartWaveDot = NSColor(calibratedWhite: 0.82, alpha: 0.55)
+
+    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 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 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 orange = NSColor(red: 0.96, green: 0.52, blue: 0.18, alpha: 1)
@@ -53,7 +128,58 @@ enum AppTheme {
     static let paywallPinkText = NSColor(red: 0.75, green: 0.30, blue: 0.45, 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 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 let paywallGoldText = NSColor(red: 0.65, green: 0.48, blue: 0.22, alpha: 1)
-    static let paywallBorder = NSColor(calibratedWhite: 0.88, alpha: 1)
+
+    static var paywallBorder: NSColor { border }
+
+    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 {
     static func semiboldFont(size: CGFloat) -> NSFont {
         .systemFont(ofSize: size, weight: .semibold)
         .systemFont(ofSize: size, weight: .semibold)
@@ -68,11 +194,15 @@ enum AppTheme {
     }
     }
 }
 }
 
 
+protocol AppearanceRefreshable: AnyObject {
+    func refreshAppearance()
+}
+
 extension NSView {
 extension NSView {
     func applyCardShadow() {
     func applyCardShadow() {
         wantsLayer = true
         wantsLayer = true
         layer?.shadowColor = NSColor.black.cgColor
         layer?.shadowColor = NSColor.black.cgColor
-        layer?.shadowOpacity = 0.07
+        layer?.shadowOpacity = AppSettings.darkModeEnabled ? 0.25 : 0.07
         layer?.shadowOffset = NSSize(width: 0, height: -3)
         layer?.shadowOffset = NSSize(width: 0, height: -3)
         layer?.shadowRadius = 14
         layer?.shadowRadius = 14
         layer?.masksToBounds = false
         layer?.masksToBounds = false
@@ -83,4 +213,66 @@ extension NSView {
         layer?.cornerRadius = radius
         layer?.cornerRadius = radius
         layer?.masksToBounds = true
         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) }
+    }
 }
 }

+ 88 - 42
smart_printer/PaywallView.swift

@@ -42,19 +42,20 @@ enum PaywallPlan: CaseIterable {
 
 
 // MARK: - Left Panel
 // MARK: - Left Panel
 
 
-private final class PaywallLeftPanelView: NSView {
+private final class PaywallLeftPanelView: NSView, AppearanceRefreshable {
     private let gradientLayer = CAGradientLayer()
     private let gradientLayer = CAGradientLayer()
 
 
     override init(frame frameRect: NSRect) {
     override init(frame frameRect: NSRect) {
         super.init(frame: frameRect)
         super.init(frame: frameRect)
         wantsLayer = true
         wantsLayer = true
-        gradientLayer.colors = [
-            NSColor(red: 0.88, green: 0.94, blue: 1.0, alpha: 1).cgColor,
-            NSColor(red: 0.95, green: 0.97, blue: 1.0, alpha: 1).cgColor,
-        ]
         gradientLayer.startPoint = CGPoint(x: 0.5, y: 1)
         gradientLayer.startPoint = CGPoint(x: 0.5, y: 1)
         gradientLayer.endPoint = CGPoint(x: 0.5, y: 0)
         gradientLayer.endPoint = CGPoint(x: 0.5, y: 0)
         layer?.insertSublayer(gradientLayer, at: 0)
         layer?.insertSublayer(gradientLayer, at: 0)
+        refreshAppearance()
+    }
+
+    func refreshAppearance() {
+        gradientLayer.colors = AppTheme.paywallLeftGradientColors.map(\.cgColor)
     }
     }
 
 
     @available(*, unavailable)
     @available(*, unavailable)
@@ -121,8 +122,11 @@ private final class PaywallBadgeView: NSView {
 
 
 // MARK: - Feature Row
 // MARK: - Feature Row
 
 
-private final class PaywallFeatureRow: NSView {
+private final class PaywallFeatureRow: NSView, AppearanceRefreshable {
+    private let label: NSTextField
+
     init(text: String) {
     init(text: String) {
+        label = NSTextField.themeLabel(text, style: .primary, font: AppTheme.regularFont(size: 14))
         super.init(frame: .zero)
         super.init(frame: .zero)
         translatesAutoresizingMaskIntoConstraints = false
         translatesAutoresizingMaskIntoConstraints = false
 
 
@@ -141,9 +145,6 @@ private final class PaywallFeatureRow: NSView {
         }
         }
         checkIcon.contentTintColor = .white
         checkIcon.contentTintColor = .white
 
 
-        let label = NSTextField(labelWithString: text)
-        label.font = AppTheme.regularFont(size: 14)
-        label.textColor = AppTheme.navy
         label.translatesAutoresizingMaskIntoConstraints = false
         label.translatesAutoresizingMaskIntoConstraints = false
 
 
         addSubview(checkContainer)
         addSubview(checkContainer)
@@ -171,11 +172,15 @@ private final class PaywallFeatureRow: NSView {
 
 
     @available(*, unavailable)
     @available(*, unavailable)
     required init?(coder: NSCoder) { nil }
     required init?(coder: NSCoder) { nil }
+
+    func refreshAppearance() {
+        label.refreshThemeLabelColor()
+    }
 }
 }
 
 
 // MARK: - Plan Card
 // MARK: - Plan Card
 
 
-private final class PaywallPlanCard: NSControl {
+private final class PaywallPlanCard: NSControl, AppearanceRefreshable {
     var onSelect: (() -> Void)?
     var onSelect: (() -> Void)?
 
 
     private let plan: PaywallPlan
     private let plan: PaywallPlan
@@ -194,7 +199,6 @@ private final class PaywallPlanCard: NSControl {
         translatesAutoresizingMaskIntoConstraints = false
         translatesAutoresizingMaskIntoConstraints = false
         wantsLayer = true
         wantsLayer = true
         layer?.cornerRadius = 12
         layer?.cornerRadius = 12
-        layer?.backgroundColor = NSColor.white.cgColor
         layer?.masksToBounds = false
         layer?.masksToBounds = false
 
 
         titleLabel.stringValue = plan.title
         titleLabel.stringValue = plan.title
@@ -203,6 +207,7 @@ private final class PaywallPlanCard: NSControl {
 
 
         subtitleLabel.stringValue = plan.subtitle
         subtitleLabel.stringValue = plan.subtitle
         subtitleLabel.font = AppTheme.regularFont(size: 11)
         subtitleLabel.font = AppTheme.regularFont(size: 11)
+        subtitleLabel.themeLabelStyle = .secondary
         subtitleLabel.textColor = AppTheme.textSecondary
         subtitleLabel.textColor = AppTheme.textSecondary
         subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
         subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
 
 
@@ -263,11 +268,17 @@ private final class PaywallPlanCard: NSControl {
     @available(*, unavailable)
     @available(*, unavailable)
     required init?(coder: NSCoder) { nil }
     required init?(coder: NSCoder) { nil }
 
 
+    func refreshAppearance() {
+        updateAppearance()
+        subtitleLabel.refreshThemeLabelColor()
+    }
+
     private func updateAppearance() {
     private func updateAppearance() {
-        let titleColor = isChosen && plan == .yearly ? AppTheme.green : AppTheme.navy
+        let titleColor = isChosen && plan == .yearly ? AppTheme.green : AppTheme.paywallAccent
         titleLabel.textColor = titleColor
         titleLabel.textColor = titleColor
         priceLabel.textColor = titleColor
         priceLabel.textColor = titleColor
 
 
+        layer?.backgroundColor = AppTheme.paywallTrustBackground.cgColor
         layer?.borderWidth = isChosen ? 2 : 1
         layer?.borderWidth = isChosen ? 2 : 1
         layer?.borderColor = (isChosen ? AppTheme.green : AppTheme.paywallBorder).cgColor
         layer?.borderColor = (isChosen ? AppTheme.green : AppTheme.paywallBorder).cgColor
     }
     }
@@ -284,19 +295,23 @@ private final class PaywallPlanCard: NSControl {
 
 
 // MARK: - Footer Link
 // MARK: - Footer Link
 
 
-private final class PaywallFooterLink: NSButton {
+private final class PaywallFooterLink: NSButton, AppearanceRefreshable {
     init(title: String) {
     init(title: String) {
         super.init(frame: .zero)
         super.init(frame: .zero)
         self.title = title
         self.title = title
         isBordered = false
         isBordered = false
         font = AppTheme.regularFont(size: 11)
         font = AppTheme.regularFont(size: 11)
-        contentTintColor = AppTheme.textSecondary
         translatesAutoresizingMaskIntoConstraints = false
         translatesAutoresizingMaskIntoConstraints = false
+        refreshAppearance()
     }
     }
 
 
     @available(*, unavailable)
     @available(*, unavailable)
     required init?(coder: NSCoder) { nil }
     required init?(coder: NSCoder) { nil }
 
 
+    func refreshAppearance() {
+        contentTintColor = AppTheme.textSecondary
+    }
+
     override func resetCursorRects() {
     override func resetCursorRects() {
         addCursorRect(bounds, cursor: .pointingHand)
         addCursorRect(bounds, cursor: .pointingHand)
     }
     }
@@ -304,36 +319,33 @@ private final class PaywallFooterLink: NSButton {
 
 
 // MARK: - Footer Trust Item
 // MARK: - Footer Trust Item
 
 
-private final class PaywallTrustItemView: NSView {
+private final class PaywallTrustItemView: NSView, AppearanceRefreshable {
+    private let iconContainer = NSView()
+    private let icon = NSImageView()
+    private let titleLabel: NSTextField
+    private let subtitleLabel: NSTextField
+
     init(iconName: String, title: String, subtitle: String) {
     init(iconName: String, title: String, subtitle: String) {
+        titleLabel = NSTextField.themeLabel(title, style: .primary, font: AppTheme.semiboldFont(size: 11))
+        subtitleLabel = NSTextField.themeLabel(subtitle, style: .secondary, font: AppTheme.regularFont(size: 9))
         super.init(frame: .zero)
         super.init(frame: .zero)
         translatesAutoresizingMaskIntoConstraints = false
         translatesAutoresizingMaskIntoConstraints = false
 
 
-        let iconContainer = NSView()
         iconContainer.translatesAutoresizingMaskIntoConstraints = false
         iconContainer.translatesAutoresizingMaskIntoConstraints = false
         iconContainer.wantsLayer = true
         iconContainer.wantsLayer = true
-        iconContainer.layer?.backgroundColor = AppTheme.blueLight.cgColor
         iconContainer.layer?.cornerRadius = 10
         iconContainer.layer?.cornerRadius = 10
         iconContainer.layer?.masksToBounds = true
         iconContainer.layer?.masksToBounds = true
 
 
-        let icon = NSImageView()
         icon.translatesAutoresizingMaskIntoConstraints = false
         icon.translatesAutoresizingMaskIntoConstraints = false
         if let image = NSImage(systemSymbolName: iconName, accessibilityDescription: nil) {
         if let image = NSImage(systemSymbolName: iconName, accessibilityDescription: nil) {
             let config = NSImage.SymbolConfiguration(pointSize: 11, weight: .semibold)
             let config = NSImage.SymbolConfiguration(pointSize: 11, weight: .semibold)
             icon.image = image.withSymbolConfiguration(config)
             icon.image = image.withSymbolConfiguration(config)
         }
         }
-        icon.contentTintColor = AppTheme.navy
 
 
-        let titleLabel = NSTextField(labelWithString: title)
-        titleLabel.font = AppTheme.semiboldFont(size: 11)
-        titleLabel.textColor = AppTheme.navy
         titleLabel.lineBreakMode = .byTruncatingTail
         titleLabel.lineBreakMode = .byTruncatingTail
         titleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
         titleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
         titleLabel.translatesAutoresizingMaskIntoConstraints = false
         titleLabel.translatesAutoresizingMaskIntoConstraints = false
 
 
-        let subtitleLabel = NSTextField(labelWithString: subtitle)
-        subtitleLabel.font = AppTheme.regularFont(size: 9)
-        subtitleLabel.textColor = AppTheme.textSecondary
         subtitleLabel.lineBreakMode = .byTruncatingTail
         subtitleLabel.lineBreakMode = .byTruncatingTail
         subtitleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
         subtitleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
         subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
         subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
@@ -366,15 +378,23 @@ private final class PaywallTrustItemView: NSView {
 
 
         setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
         setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
         setContentHuggingPriority(.defaultLow, for: .horizontal)
         setContentHuggingPriority(.defaultLow, for: .horizontal)
+        refreshAppearance()
     }
     }
 
 
     @available(*, unavailable)
     @available(*, unavailable)
     required init?(coder: NSCoder) { nil }
     required init?(coder: NSCoder) { nil }
+
+    func refreshAppearance() {
+        iconContainer.layer?.backgroundColor = AppTheme.paywallTrustIconBackground.cgColor
+        icon.contentTintColor = AppTheme.paywallIconAccent
+        titleLabel.refreshThemeLabelColor()
+        subtitleLabel.refreshThemeLabelColor()
+    }
 }
 }
 
 
 // MARK: - Main Paywall Card
 // MARK: - Main Paywall Card
 
 
-final class PaywallView: NSView {
+final class PaywallView: NSView, AppearanceRefreshable {
     var onClose: (() -> Void)?
     var onClose: (() -> Void)?
     var onPurchase: ((PaywallPlan) -> Void)?
     var onPurchase: ((PaywallPlan) -> Void)?
     var onRestore: (() -> Void)?
     var onRestore: (() -> Void)?
@@ -382,14 +402,30 @@ final class PaywallView: NSView {
     private var selectedPlan: PaywallPlan = .yearly
     private var selectedPlan: PaywallPlan = .yearly
     private var planCards: [PaywallPlan: PaywallPlanCard] = [:]
     private var planCards: [PaywallPlan: PaywallPlanCard] = [:]
     private let ctaButton = NSButton()
     private let ctaButton = NSButton()
+    private var leftPanelTitle: NSTextField!
+    private var rightTitle: NSTextField!
+    private var rightSubtitle: NSTextField!
+    private var trustStack: NSStackView!
 
 
     init() {
     init() {
         super.init(frame: .zero)
         super.init(frame: .zero)
         translatesAutoresizingMaskIntoConstraints = false
         translatesAutoresizingMaskIntoConstraints = false
         wantsLayer = true
         wantsLayer = true
-        layer?.backgroundColor = NSColor.white.cgColor
         layer?.cornerRadius = 0
         layer?.cornerRadius = 0
         setup()
         setup()
+        refreshAppearance()
+    }
+
+    func refreshAppearance() {
+        layer?.backgroundColor = AppTheme.paywallBackground.cgColor
+        leftPanelTitle?.refreshThemeLabelColor()
+        rightTitle?.refreshThemeLabelColor()
+        rightSubtitle?.refreshThemeLabelColor()
+        trustStack?.layer?.backgroundColor = AppTheme.paywallTrustBackground.cgColor
+        trustStack?.layer?.borderColor = AppTheme.paywallBorder.cgColor
+        ctaButton.layer?.backgroundColor = AppTheme.paywallCTABackground.cgColor
+        ctaButton.contentTintColor = AppTheme.paywallCTAForeground
+        subviews.forEach { $0.refreshAppearanceRecursively() }
     }
     }
 
 
     @available(*, unavailable)
     @available(*, unavailable)
@@ -419,11 +455,14 @@ final class PaywallView: NSView {
         let panel = PaywallLeftPanelView()
         let panel = PaywallLeftPanelView()
         panel.translatesAutoresizingMaskIntoConstraints = false
         panel.translatesAutoresizingMaskIntoConstraints = false
 
 
-        let title = NSTextField(labelWithString: "Unlock Your Full\nPrinting Potential")
-        title.font = AppTheme.semiboldFont(size: 22)
-        title.textColor = AppTheme.navy
+        let title = NSTextField.themeLabel(
+            "Unlock Your Full\nPrinting Potential",
+            style: .primary,
+            font: AppTheme.semiboldFont(size: 22)
+        )
         title.maximumNumberOfLines = 2
         title.maximumNumberOfLines = 2
         title.translatesAutoresizingMaskIntoConstraints = false
         title.translatesAutoresizingMaskIntoConstraints = false
+        leftPanelTitle = title
 
 
         let featuresStack = NSStackView()
         let featuresStack = NSStackView()
         featuresStack.orientation = .vertical
         featuresStack.orientation = .vertical
@@ -463,18 +502,20 @@ final class PaywallView: NSView {
         let panel = NSView()
         let panel = NSView()
         panel.translatesAutoresizingMaskIntoConstraints = false
         panel.translatesAutoresizingMaskIntoConstraints = false
 
 
-        let title = NSTextField(labelWithString: "Go Premium")
-        title.font = AppTheme.semiboldFont(size: 26)
-        title.textColor = AppTheme.navy
+        let title = NSTextField.themeLabel("Go Premium", style: .primary, font: AppTheme.semiboldFont(size: 26))
         title.alignment = .center
         title.alignment = .center
         title.translatesAutoresizingMaskIntoConstraints = false
         title.translatesAutoresizingMaskIntoConstraints = false
+        rightTitle = title
 
 
-        let subtitle = NSTextField(labelWithString: "Experience professional quality printing and scanning without limits.")
-        subtitle.font = AppTheme.regularFont(size: 13)
-        subtitle.textColor = AppTheme.textSecondary
+        let subtitle = NSTextField.themeLabel(
+            "Experience professional quality printing and scanning without limits.",
+            style: .secondary,
+            font: AppTheme.regularFont(size: 13)
+        )
         subtitle.alignment = .center
         subtitle.alignment = .center
         subtitle.maximumNumberOfLines = 2
         subtitle.maximumNumberOfLines = 2
         subtitle.translatesAutoresizingMaskIntoConstraints = false
         subtitle.translatesAutoresizingMaskIntoConstraints = false
+        rightSubtitle = subtitle
 
 
         let plansStack = NSStackView()
         let plansStack = NSStackView()
         plansStack.orientation = .vertical
         plansStack.orientation = .vertical
@@ -492,10 +533,8 @@ final class PaywallView: NSView {
         ctaButton.title = selectedPlan.ctaTitle
         ctaButton.title = selectedPlan.ctaTitle
         ctaButton.isBordered = false
         ctaButton.isBordered = false
         ctaButton.wantsLayer = true
         ctaButton.wantsLayer = true
-        ctaButton.layer?.backgroundColor = AppTheme.navy.cgColor
         ctaButton.layer?.cornerRadius = 12
         ctaButton.layer?.cornerRadius = 12
         ctaButton.font = AppTheme.semiboldFont(size: 15)
         ctaButton.font = AppTheme.semiboldFont(size: 15)
-        ctaButton.contentTintColor = .white
         ctaButton.target = self
         ctaButton.target = self
         ctaButton.action = #selector(purchaseTapped)
         ctaButton.action = #selector(purchaseTapped)
         ctaButton.translatesAutoresizingMaskIntoConstraints = false
         ctaButton.translatesAutoresizingMaskIntoConstraints = false
@@ -569,13 +608,12 @@ final class PaywallView: NSView {
         trustStack.alignment = .top
         trustStack.alignment = .top
         trustStack.translatesAutoresizingMaskIntoConstraints = false
         trustStack.translatesAutoresizingMaskIntoConstraints = false
         trustStack.wantsLayer = true
         trustStack.wantsLayer = true
-        trustStack.layer?.backgroundColor = NSColor.white.cgColor
         trustStack.layer?.cornerRadius = 12
         trustStack.layer?.cornerRadius = 12
         trustStack.layer?.borderWidth = 1
         trustStack.layer?.borderWidth = 1
-        trustStack.layer?.borderColor = AppTheme.paywallBorder.cgColor
         trustStack.layer?.masksToBounds = true
         trustStack.layer?.masksToBounds = true
         trustStack.edgeInsets = NSEdgeInsets(top: 12, left: 14, bottom: 12, right: 14)
         trustStack.edgeInsets = NSEdgeInsets(top: 12, left: 14, bottom: 12, right: 14)
         trustStack.translatesAutoresizingMaskIntoConstraints = false
         trustStack.translatesAutoresizingMaskIntoConstraints = false
+        self.trustStack = trustStack
 
 
         return trustStack
         return trustStack
     }
     }
@@ -657,18 +695,27 @@ final class PaywallView: NSView {
 
 
 // MARK: - Overlay Presenter
 // MARK: - Overlay Presenter
 
 
-final class PaywallOverlayView: NSView {
+final class PaywallOverlayView: NSView, AppearanceRefreshable {
     var onDismiss: (() -> Void)?
     var onDismiss: (() -> Void)?
 
 
     private let paywallView: PaywallView
     private let paywallView: PaywallView
     private let blurView = NSVisualEffectView()
     private let blurView = NSVisualEffectView()
     private let backdrop = NSView()
     private let backdrop = NSView()
+    private let pattern = WavePatternView()
 
 
     init() {
     init() {
         paywallView = PaywallView()
         paywallView = PaywallView()
         super.init(frame: .zero)
         super.init(frame: .zero)
         translatesAutoresizingMaskIntoConstraints = false
         translatesAutoresizingMaskIntoConstraints = false
         setup()
         setup()
+        refreshAppearance()
+    }
+
+    func refreshAppearance() {
+        backdrop.layer?.backgroundColor = AppTheme.paywallOverlayBackdrop.cgColor
+        blurView.material = AppSettings.darkModeEnabled ? .hudWindow : .underWindowBackground
+        pattern.refreshAppearance()
+        paywallView.refreshAppearance()
     }
     }
 
 
     @available(*, unavailable)
     @available(*, unavailable)
@@ -684,7 +731,6 @@ final class PaywallOverlayView: NSView {
         backdrop.wantsLayer = true
         backdrop.wantsLayer = true
         backdrop.layer?.backgroundColor = NSColor(calibratedWhite: 0.15, alpha: 0.22).cgColor
         backdrop.layer?.backgroundColor = NSColor(calibratedWhite: 0.15, alpha: 0.22).cgColor
 
 
-        let pattern = WavePatternView()
         pattern.translatesAutoresizingMaskIntoConstraints = false
         pattern.translatesAutoresizingMaskIntoConstraints = false
         pattern.alphaValue = 0.35
         pattern.alphaValue = 0.35
 
 

+ 28 - 18
smart_printer/SettingsView.swift

@@ -2,7 +2,7 @@ import Cocoa
 
 
 // MARK: - Root
 // MARK: - Root
 
 
-final class SettingsView: NSView {
+final class SettingsView: NSView, AppearanceRefreshable {
     init() {
     init() {
         super.init(frame: .zero)
         super.init(frame: .zero)
         wantsLayer = true
         wantsLayer = true
@@ -14,6 +14,10 @@ final class SettingsView: NSView {
     @available(*, unavailable)
     @available(*, unavailable)
     required init?(coder: NSCoder) { nil }
     required init?(coder: NSCoder) { nil }
 
 
+    func refreshAppearance() {
+        layer?.backgroundColor = AppTheme.background.cgColor
+    }
+
     private func setup() {
     private func setup() {
         let scrollView = NSScrollView()
         let scrollView = NSScrollView()
         scrollView.translatesAutoresizingMaskIntoConstraints = false
         scrollView.translatesAutoresizingMaskIntoConstraints = false
@@ -81,9 +85,7 @@ final class SettingsView: NSView {
         container.spacing = 12
         container.spacing = 12
         container.translatesAutoresizingMaskIntoConstraints = false
         container.translatesAutoresizingMaskIntoConstraints = false
 
 
-        let label = NSTextField(labelWithString: title)
-        label.font = AppTheme.semiboldFont(size: 18)
-        label.textColor = AppTheme.textPrimary
+        let label = NSTextField.themeLabel(title, style: .primary, font: AppTheme.semiboldFont(size: 18))
         label.translatesAutoresizingMaskIntoConstraints = false
         label.translatesAutoresizingMaskIntoConstraints = false
 
 
         container.addArrangedSubview(label)
         container.addArrangedSubview(label)
@@ -137,32 +139,35 @@ final class SettingsView: NSView {
 
 
 // MARK: - Panel
 // MARK: - Panel
 
 
-private final class SettingsPanelView: NSView {
+private final class SettingsPanelView: NSView, AppearanceRefreshable {
     init() {
     init() {
         super.init(frame: .zero)
         super.init(frame: .zero)
         wantsLayer = true
         wantsLayer = true
-        layer?.backgroundColor = AppTheme.cardBackground.cgColor
         layer?.cornerRadius = 22
         layer?.cornerRadius = 22
         layer?.borderWidth = 1
         layer?.borderWidth = 1
-        layer?.borderColor = NSColor(calibratedWhite: 0.92, alpha: 1).cgColor
         applyCardShadow()
         applyCardShadow()
+        refreshAppearance()
+    }
+
+    func refreshAppearance() {
+        layer?.backgroundColor = AppTheme.cardBackground.cgColor
+        layer?.borderColor = AppTheme.border.cgColor
     }
     }
 
 
     @available(*, unavailable)
     @available(*, unavailable)
     required init?(coder: NSCoder) { nil }
     required init?(coder: NSCoder) { nil }
 }
 }
 
 
-private final class SettingsGroupCard: NSView {
+private final class SettingsGroupCard: NSView, AppearanceRefreshable {
     private let stack = NSStackView()
     private let stack = NSStackView()
 
 
     init() {
     init() {
         super.init(frame: .zero)
         super.init(frame: .zero)
         translatesAutoresizingMaskIntoConstraints = false
         translatesAutoresizingMaskIntoConstraints = false
         wantsLayer = true
         wantsLayer = true
-        layer?.backgroundColor = NSColor.white.cgColor
         layer?.cornerRadius = 16
         layer?.cornerRadius = 16
         layer?.borderWidth = 1
         layer?.borderWidth = 1
-        layer?.borderColor = NSColor(calibratedWhite: 0.93, alpha: 1).cgColor
+        refreshAppearance()
 
 
         stack.orientation = .vertical
         stack.orientation = .vertical
         stack.spacing = 0
         stack.spacing = 0
@@ -184,17 +189,22 @@ private final class SettingsGroupCard: NSView {
         stack.addArrangedSubview(row)
         stack.addArrangedSubview(row)
         row.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
         row.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
     }
     }
+
+    func refreshAppearance() {
+        layer?.backgroundColor = AppTheme.groupCardBackground.cgColor
+        layer?.borderColor = AppTheme.border.cgColor
+    }
 }
 }
 
 
 // MARK: - Icon
 // MARK: - Icon
 
 
-private final class SettingsIconBadge: NSView {
+private final class SettingsIconBadge: NSView, AppearanceRefreshable {
     init(symbolName: String) {
     init(symbolName: String) {
         super.init(frame: .zero)
         super.init(frame: .zero)
         translatesAutoresizingMaskIntoConstraints = false
         translatesAutoresizingMaskIntoConstraints = false
         wantsLayer = true
         wantsLayer = true
-        layer?.backgroundColor = AppTheme.blueLight.cgColor
         layer?.cornerRadius = 10
         layer?.cornerRadius = 10
+        refreshAppearance()
 
 
         let icon = NSImageView()
         let icon = NSImageView()
         icon.translatesAutoresizingMaskIntoConstraints = false
         icon.translatesAutoresizingMaskIntoConstraints = false
@@ -217,6 +227,10 @@ private final class SettingsIconBadge: NSView {
 
 
     @available(*, unavailable)
     @available(*, unavailable)
     required init?(coder: NSCoder) { nil }
     required init?(coder: NSCoder) { nil }
+
+    func refreshAppearance() {
+        layer?.backgroundColor = AppTheme.blueLight.cgColor
+    }
 }
 }
 
 
 // MARK: - Rows
 // MARK: - Rows
@@ -247,9 +261,7 @@ private class SettingsRowBase: NSView {
 
 
     func install(icon symbolName: String, title: String, trailing: NSView) -> NSTextField {
     func install(icon symbolName: String, title: String, trailing: NSView) -> NSTextField {
         let badge = SettingsIconBadge(symbolName: symbolName)
         let badge = SettingsIconBadge(symbolName: symbolName)
-        let titleLabel = NSTextField(labelWithString: title)
-        titleLabel.font = AppTheme.mediumFont(size: 15)
-        titleLabel.textColor = AppTheme.textPrimary
+        let titleLabel = NSTextField.themeLabel(title, style: .primary, font: AppTheme.mediumFont(size: 15))
         titleLabel.translatesAutoresizingMaskIntoConstraints = false
         titleLabel.translatesAutoresizingMaskIntoConstraints = false
         trailing.translatesAutoresizingMaskIntoConstraints = false
         trailing.translatesAutoresizingMaskIntoConstraints = false
 
 
@@ -278,9 +290,7 @@ private final class SettingsActionRow: SettingsRowBase {
         super.init(isLast: isLast)
         super.init(isLast: isLast)
 
 
         let badge = SettingsIconBadge(symbolName: symbolName)
         let badge = SettingsIconBadge(symbolName: symbolName)
-        let titleLabel = NSTextField(labelWithString: title)
-        titleLabel.font = AppTheme.mediumFont(size: 15)
-        titleLabel.textColor = AppTheme.textPrimary
+        let titleLabel = NSTextField.themeLabel(title, style: .primary, font: AppTheme.mediumFont(size: 15))
         titleLabel.translatesAutoresizingMaskIntoConstraints = false
         titleLabel.translatesAutoresizingMaskIntoConstraints = false
 
 
         addSubview(badge)
         addSubview(badge)

+ 8 - 4
smart_printer/SidebarView.swift

@@ -7,7 +7,8 @@ enum SidebarDestination: Int, CaseIterable {
     case settings
     case settings
 }
 }
 
 
-final class SidebarView: NSView {
+final class SidebarView: NSView, AppearanceRefreshable {
+    private var appNameLabel: NSTextField!
     var onDestinationSelected: ((SidebarDestination) -> Void)?
     var onDestinationSelected: ((SidebarDestination) -> Void)?
 
 
     private var navItems: [SidebarNavItem] = []
     private var navItems: [SidebarNavItem] = []
@@ -29,6 +30,11 @@ final class SidebarView: NSView {
         }
         }
     }
     }
 
 
+    func refreshAppearance() {
+        layer?.backgroundColor = AppTheme.sidebarBackground.cgColor
+        appNameLabel?.refreshThemeLabelColor()
+    }
+
     private func setup() {
     private func setup() {
         let logoContainer = NSView()
         let logoContainer = NSView()
         logoContainer.translatesAutoresizingMaskIntoConstraints = false
         logoContainer.translatesAutoresizingMaskIntoConstraints = false
@@ -41,9 +47,7 @@ final class SidebarView: NSView {
         }
         }
         logoIcon.contentTintColor = AppTheme.purple
         logoIcon.contentTintColor = AppTheme.purple
 
 
-        let appNameLabel = NSTextField(labelWithString: "Smart Printer")
-        appNameLabel.font = AppTheme.semiboldFont(size: 15)
-        appNameLabel.textColor = AppTheme.textPrimary
+        appNameLabel = NSTextField.themeLabel("Smart Printer", style: .primary, font: AppTheme.semiboldFont(size: 15))
         appNameLabel.alignment = .center
         appNameLabel.alignment = .center
         appNameLabel.translatesAutoresizingMaskIntoConstraints = false
         appNameLabel.translatesAutoresizingMaskIntoConstraints = false
 
 

+ 34 - 17
smart_printer/UIComponents.swift

@@ -28,15 +28,21 @@ final class GradientCardView: NSView {
 
 
 // MARK: - Wave Pattern
 // MARK: - Wave Pattern
 
 
-final class WavePatternView: NSView {
+final class WavePatternView: NSView, AppearanceRefreshable {
+    var fixedDotColor: NSColor?
+
     override var isOpaque: Bool { false }
     override var isOpaque: Bool { false }
 
 
+    func refreshAppearance() {
+        guard fixedDotColor == nil else { return }
+        needsDisplay = true
+    }
+
     override func draw(_ dirtyRect: NSRect) {
     override func draw(_ dirtyRect: NSRect) {
         super.draw(dirtyRect)
         super.draw(dirtyRect)
         guard let context = NSGraphicsContext.current?.cgContext else { return }
         guard let context = NSGraphicsContext.current?.cgContext else { return }
 
 
-        let dotColor = NSColor(calibratedWhite: 0.82, alpha: 0.55)
-        context.setFillColor(dotColor.cgColor)
+        context.setFillColor((fixedDotColor ?? AppTheme.waveDotColor).cgColor)
 
 
         let spacing: CGFloat = 14
         let spacing: CGFloat = 14
         let dotSize: CGFloat = 3
         let dotSize: CGFloat = 3
@@ -57,7 +63,7 @@ final class WavePatternView: NSView {
 
 
 // MARK: - Sidebar Nav Item
 // MARK: - Sidebar Nav Item
 
 
-final class SidebarNavItem: NSControl {
+final class SidebarNavItem: NSControl, AppearanceRefreshable {
     enum Style {
     enum Style {
         case normal
         case normal
         case active
         case active
@@ -86,6 +92,7 @@ final class SidebarNavItem: NSControl {
 
 
         titleLabel.stringValue = title
         titleLabel.stringValue = title
         titleLabel.font = AppTheme.mediumFont(size: 14)
         titleLabel.font = AppTheme.mediumFont(size: 14)
+        titleLabel.themeLabelStyle = .primary
         titleLabel.textColor = AppTheme.textPrimary
         titleLabel.textColor = AppTheme.textPrimary
 
 
         if let image = NSImage(systemSymbolName: symbolName, accessibilityDescription: title) {
         if let image = NSImage(systemSymbolName: symbolName, accessibilityDescription: title) {
@@ -127,6 +134,10 @@ final class SidebarNavItem: NSControl {
     @available(*, unavailable)
     @available(*, unavailable)
     required init?(coder: NSCoder) { nil }
     required init?(coder: NSCoder) { nil }
 
 
+    func refreshAppearance() {
+        applyStyle(isSelected ? .active : .normal)
+    }
+
     private func applyStyle(_ style: Style) {
     private func applyStyle(_ style: Style) {
         container.wantsLayer = true
         container.wantsLayer = true
         container.layer?.cornerRadius = AppTheme.cornerRadius
         container.layer?.cornerRadius = AppTheme.cornerRadius
@@ -296,13 +307,14 @@ final class QuickStartCardView: NSView {
 
 
         let subtitleLabel = NSTextField(labelWithString: data.subtitle)
         let subtitleLabel = NSTextField(labelWithString: data.subtitle)
         subtitleLabel.font = AppTheme.regularFont(size: 13)
         subtitleLabel.font = AppTheme.regularFont(size: 13)
-        subtitleLabel.textColor = AppTheme.textSecondary
+        subtitleLabel.textColor = AppTheme.quickStartCardSubtitle
         subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
         subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
 
 
         let button = PillButton(title: data.buttonTitle, color: data.accentColor)
         let button = PillButton(title: data.buttonTitle, color: data.accentColor)
         button.translatesAutoresizingMaskIntoConstraints = false
         button.translatesAutoresizingMaskIntoConstraints = false
 
 
         let wavePattern = WavePatternView()
         let wavePattern = WavePatternView()
+        wavePattern.fixedDotColor = AppTheme.quickStartWaveDot
         wavePattern.translatesAutoresizingMaskIntoConstraints = false
         wavePattern.translatesAutoresizingMaskIntoConstraints = false
 
 
         addSubview(gradient)
         addSubview(gradient)
@@ -365,39 +377,34 @@ struct FeatureCardData {
     let iconKind: FeatureIconKind
     let iconKind: FeatureIconKind
 }
 }
 
 
-final class FeatureCardView: NSView {
+final class FeatureCardView: NSView, AppearanceRefreshable {
     private let iconView: FeatureIconView
     private let iconView: FeatureIconView
+    private let titleLabel: NSTextField
+    private let subtitleLabel: NSTextField
+    private let arrowButton: NSButton
     private var iconWidthConstraint: NSLayoutConstraint!
     private var iconWidthConstraint: NSLayoutConstraint!
     private var iconHeightConstraint: NSLayoutConstraint!
     private var iconHeightConstraint: NSLayoutConstraint!
 
 
     init(data: FeatureCardData) {
     init(data: FeatureCardData) {
         iconView = FeatureIconView(kind: data.iconKind)
         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)
         super.init(frame: .zero)
         translatesAutoresizingMaskIntoConstraints = false
         translatesAutoresizingMaskIntoConstraints = false
         setContentHuggingPriority(.defaultLow, for: .horizontal)
         setContentHuggingPriority(.defaultLow, for: .horizontal)
         setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
         setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
         wantsLayer = true
         wantsLayer = true
-        layer?.backgroundColor = AppTheme.cardBackground.cgColor
         layer?.cornerRadius = AppTheme.featureCardCornerRadius
         layer?.cornerRadius = AppTheme.featureCardCornerRadius
         applyCardShadow()
         applyCardShadow()
 
 
-        let titleLabel = NSTextField(labelWithString: data.title)
-        titleLabel.font = AppTheme.semiboldFont(size: 14)
-        titleLabel.textColor = AppTheme.textPrimary
         titleLabel.translatesAutoresizingMaskIntoConstraints = false
         titleLabel.translatesAutoresizingMaskIntoConstraints = false
-
-        let subtitleLabel = NSTextField(labelWithString: data.subtitle)
-        subtitleLabel.font = AppTheme.regularFont(size: 11)
-        subtitleLabel.textColor = AppTheme.textSecondary
         subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
         subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
 
 
-        let arrowButton = NSButton()
         arrowButton.isBordered = false
         arrowButton.isBordered = false
         arrowButton.wantsLayer = true
         arrowButton.wantsLayer = true
-        arrowButton.layer?.backgroundColor = NSColor.white.cgColor
         arrowButton.layer?.cornerRadius = 13
         arrowButton.layer?.cornerRadius = 13
         arrowButton.layer?.borderWidth = 1
         arrowButton.layer?.borderWidth = 1
-        arrowButton.layer?.borderColor = NSColor(calibratedWhite: 0.88, alpha: 1).cgColor
         arrowButton.translatesAutoresizingMaskIntoConstraints = false
         arrowButton.translatesAutoresizingMaskIntoConstraints = false
         if let arrow = NSImage(systemSymbolName: "chevron.right", accessibilityDescription: "Open") {
         if let arrow = NSImage(systemSymbolName: "chevron.right", accessibilityDescription: "Open") {
             let config = NSImage.SymbolConfiguration(pointSize: 10, weight: .semibold)
             let config = NSImage.SymbolConfiguration(pointSize: 10, weight: .semibold)
@@ -434,11 +441,21 @@ final class FeatureCardView: NSView {
         iconHeightConstraint = iconView.heightAnchor.constraint(equalToConstant: AppTheme.featureIconMax)
         iconHeightConstraint = iconView.heightAnchor.constraint(equalToConstant: AppTheme.featureIconMax)
         iconWidthConstraint.isActive = true
         iconWidthConstraint.isActive = true
         iconHeightConstraint.isActive = true
         iconHeightConstraint.isActive = true
+        refreshAppearance()
     }
     }
 
 
     @available(*, unavailable)
     @available(*, unavailable)
     required init?(coder: NSCoder) { nil }
     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() {
     override func layout() {
         super.layout()
         super.layout()
         let size = AppTheme.featureIconSize(forCardWidth: bounds.width)
         let size = AppTheme.featureIconSize(forCardWidth: bounds.width)

+ 23 - 7
smart_printer/ViewController.swift

@@ -38,6 +38,12 @@ class ViewController: NSViewController {
             name: .showSettings,
             name: .showSettings,
             object: nil
             object: nil
         )
         )
+        NotificationCenter.default.addObserver(
+            self,
+            selector: #selector(appearanceDidChange),
+            name: .appearanceDidChange,
+            object: nil
+        )
     }
     }
 
 
     deinit {
     deinit {
@@ -48,6 +54,19 @@ class ViewController: NSViewController {
         navigateToSettings()
         navigateToSettings()
     }
     }
 
 
+    @objc private func appearanceDidChange() {
+        refreshAppearance()
+    }
+
+    private func refreshAppearance() {
+        view.layer?.backgroundColor = AppTheme.background.cgColor
+        mainContentView?.layer?.backgroundColor = AppTheme.background.cgColor
+        view.window?.backgroundColor = AppTheme.background
+        sidebar?.refreshAppearance()
+        view.refreshAppearanceRecursively()
+        paywallOverlay?.refreshAppearance()
+    }
+
     private func setupLayout() {
     private func setupLayout() {
         sidebar = SidebarView()
         sidebar = SidebarView()
 
 
@@ -270,9 +289,7 @@ class ViewController: NSViewController {
         let header = NSView()
         let header = NSView()
         header.translatesAutoresizingMaskIntoConstraints = false
         header.translatesAutoresizingMaskIntoConstraints = false
 
 
-        let titleLabel = NSTextField(labelWithString: "Smart Printer")
-        titleLabel.font = AppTheme.semiboldFont(size: 18)
-        titleLabel.textColor = AppTheme.textPrimary
+        let titleLabel = NSTextField.themeLabel("Smart Printer", style: .primary, font: AppTheme.semiboldFont(size: 18))
         titleLabel.alignment = .center
         titleLabel.alignment = .center
         titleLabel.translatesAutoresizingMaskIntoConstraints = false
         titleLabel.translatesAutoresizingMaskIntoConstraints = false
 
 
@@ -375,6 +392,7 @@ class ViewController: NSViewController {
                     gridIcon.image = image.withSymbolConfiguration(config)
                     gridIcon.image = image.withSymbolConfiguration(config)
                 }
                 }
                 gridIcon.contentTintColor = AppTheme.textPrimary
                 gridIcon.contentTintColor = AppTheme.textPrimary
+                gridIcon.usesThemeTint = true
                 titleIcon = gridIcon
                 titleIcon = gridIcon
             case .none:
             case .none:
                 titleIcon = NSView()
                 titleIcon = NSView()
@@ -393,9 +411,7 @@ class ViewController: NSViewController {
             iconSpacing = 8
             iconSpacing = 8
         }
         }
 
 
-        let label = NSTextField(labelWithString: text)
-        label.font = .systemFont(ofSize: 20, weight: .bold)
-        label.textColor = AppTheme.textPrimary
+        let label = NSTextField.themeLabel(text, style: .primary, font: .systemFont(ofSize: 20, weight: .bold))
         label.translatesAutoresizingMaskIntoConstraints = false
         label.translatesAutoresizingMaskIntoConstraints = false
         container.addSubview(label)
         container.addSubview(label)
 
 
@@ -428,7 +444,7 @@ class ViewController: NSViewController {
                 subtitle: "Take a photo from gallery",
                 subtitle: "Take a photo from gallery",
                 buttonTitle: "Open Gallery",
                 buttonTitle: "Open Gallery",
                 accentColor: AppTheme.blue,
                 accentColor: AppTheme.blue,
-                gradientColors: [AppTheme.blueLight, NSColor(red: 0.82, green: 0.90, blue: 1.0, alpha: 1)],
+                gradientColors: [AppTheme.quickStartBlueLight, NSColor(red: 0.82, green: 0.90, blue: 1.0, alpha: 1)],
                 iconKind: .photos
                 iconKind: .photos
             ),
             ),
             QuickStartCardData(
             QuickStartCardData(