@@ -109,8 +109,9 @@ internal final class ThemeEngine {
109109
110110 private init ( ) {
111111 let allThemes = ThemeStorage . loadAllThemes ( )
112- let activeId = ThemeStorage . loadActiveThemeId ( )
113- let theme = allThemes. first { $0. id == activeId } ?? . default
112+ // Start with the default theme; AppSettingsManager.init() will call
113+ // updateAppearanceAndTheme() to activate the correct preferred theme.
114+ let theme = ThemeDefinition . default
114115
115116 self . activeTheme = theme
116117 self . colors = ResolvedThemeColors ( from: theme)
@@ -140,7 +141,6 @@ internal final class ThemeEngine {
140141 editorFonts = EditorFontCache ( from: theme. fonts)
141142 dataGridFonts = DataGridFontCacheResolved ( from: theme. fonts)
142143
143- ThemeStorage . saveActiveThemeId ( theme. id)
144144 notifyThemeDidChange ( )
145145
146146 Self . logger. info ( " Activated theme: \( theme. name) ( \( theme. id) ) " )
@@ -163,9 +163,27 @@ internal final class ThemeEngine {
163163 try ThemeStorage . deleteUserTheme ( id: id)
164164 reloadAvailableThemes ( )
165165
166- // If deleted the active theme, fall back to default
167- if id == activeTheme. id {
168- activateTheme ( id: " tablepro.default-light " )
166+ // If deleted a preferred theme, reset that slot to default
167+ var appearance = AppSettingsManager . shared. appearance
168+ var changed = false
169+ if id == appearance. preferredLightThemeId {
170+ appearance. preferredLightThemeId = " tablepro.default-light "
171+ changed = true
172+ }
173+ if id == appearance. preferredDarkThemeId {
174+ appearance. preferredDarkThemeId = " tablepro.default-dark "
175+ changed = true
176+ }
177+ if changed {
178+ AppSettingsManager . shared. appearance = appearance
179+ } else if id == activeTheme. id {
180+ // Deleted a non-preferred but currently active theme — re-anchor to preferred
181+ let appearance = AppSettingsManager . shared. appearance
182+ updateAppearanceAndTheme (
183+ mode: appearance. appearanceMode,
184+ lightThemeId: appearance. preferredLightThemeId,
185+ darkThemeId: appearance. preferredDarkThemeId
186+ )
169187 }
170188 }
171189
@@ -209,6 +227,11 @@ internal final class ThemeEngine {
209227 activeTheme = theme
210228 editorFonts = EditorFontCache ( from: theme. fonts)
211229 notifyThemeDidChange ( )
230+
231+ // Persist so the zoom survives re-activation (e.g. system appearance change)
232+ if theme. isEditable {
233+ try ? ThemeStorage . saveUserTheme ( theme)
234+ }
212235 }
213236
214237 // MARK: - Font Cache Reload (accessibility)
@@ -273,13 +296,51 @@ internal final class ThemeEngine {
273296 // MARK: - Appearance
274297
275298 @ObservationIgnored private( set) var appearanceMode : AppAppearanceMode = . auto
276-
277- func updateAppearanceMode( _ mode: AppAppearanceMode ) {
299+ private( set) var effectiveAppearance : ThemeAppearance = . light
300+ @ObservationIgnored private var currentLightThemeId : String = " tablepro.default-light "
301+ @ObservationIgnored private var currentDarkThemeId : String = " tablepro.default-dark "
302+ @ObservationIgnored private var systemAppearanceObserver : NSObjectProtocol ?
303+
304+ /// Central entry point: resolves effective appearance, picks the correct theme, activates it,
305+ /// and derives NSApp.appearance from the theme's own appearance metadata.
306+ func updateAppearanceAndTheme(
307+ mode: AppAppearanceMode ,
308+ lightThemeId: String ,
309+ darkThemeId: String
310+ ) {
278311 appearanceMode = mode
279- applyAppearance ( mode)
312+ currentLightThemeId = lightThemeId
313+ currentDarkThemeId = darkThemeId
314+
315+ let resolved = resolveEffectiveAppearance ( mode)
316+ effectiveAppearance = resolved
317+
318+ let themeId = resolved == . dark ? darkThemeId : lightThemeId
319+ activateTheme ( id: themeId)
320+ applyNSAppAppearance ( mode: mode)
321+
322+ updateSystemAppearanceObserver ( mode: mode)
280323 }
281324
282- private func applyAppearance( _ mode: AppAppearanceMode ) {
325+ /// Resolve which appearance is in effect right now.
326+ private func resolveEffectiveAppearance( _ mode: AppAppearanceMode ) -> ThemeAppearance {
327+ switch mode {
328+ case . light: return . light
329+ case . dark: return . dark
330+ case . auto: return systemIsDark ( ) ? . dark : . light
331+ }
332+ }
333+
334+ /// Check if the system is currently in dark mode.
335+ /// Reads the global `AppleInterfaceStyle` default directly so we get the real
336+ /// system setting, not the app's own forced appearance.
337+ private func systemIsDark( ) -> Bool {
338+ UserDefaults . standard. string ( forKey: " AppleInterfaceStyle " ) == " Dark "
339+ }
340+
341+ /// Set NSApp.appearance based on the appearance mode (not the theme).
342+ /// Auto mode sets nil so the system controls the chrome.
343+ private func applyNSAppAppearance( mode: AppAppearanceMode ) {
283344 switch mode {
284345 case . light:
285346 NSApp ? . appearance = NSAppearance ( named: . aqua)
@@ -290,6 +351,35 @@ internal final class ThemeEngine {
290351 }
291352 }
292353
354+ // MARK: - System Appearance Observer
355+
356+ private func updateSystemAppearanceObserver( mode: AppAppearanceMode ) {
357+ // Remove existing observer
358+ if let observer = systemAppearanceObserver {
359+ DistributedNotificationCenter . default ( ) . removeObserver ( observer)
360+ systemAppearanceObserver = nil
361+ }
362+
363+ guard mode == . auto else { return }
364+
365+ // Install observer for system appearance changes
366+ systemAppearanceObserver = DistributedNotificationCenter . default ( ) . addObserver (
367+ forName: Notification . Name ( " AppleInterfaceThemeChangedNotification " ) ,
368+ object: nil ,
369+ queue: . main
370+ ) { [ weak self] _ in
371+ Task { @MainActor [ weak self] in
372+ guard let self, self . appearanceMode == . auto else { return }
373+ let newAppearance = self . systemIsDark ( ) ? ThemeAppearance . dark : ThemeAppearance . light
374+ guard newAppearance != self . effectiveAppearance else { return }
375+ self . effectiveAppearance = newAppearance
376+ let themeId = newAppearance == . dark ? self . currentDarkThemeId : self . currentLightThemeId
377+ self . activateTheme ( id: themeId)
378+ self . applyNSAppAppearance ( mode: . auto)
379+ }
380+ }
381+ }
382+
293383 // MARK: - Notifications
294384
295385 private func notifyThemeDidChange( ) {
0 commit comments