eleven / ocho Public

mac: unpin NSApp.appearance in system mode (dark could never release) (#82)

The webview reported a RESOLVED light/dark boolean and native pinned
NSApp.appearance to match — but inside a WKWebView, prefers-color-scheme
reflects NSApp.appearance, so "follow my device" resolved against the
pinned value: once dark, system mode stayed dark forever regardless of
the actual macOS setting. Report the mode (light/dark/system) instead,
and unpin (nil) on system so the app and the page both follow the OS.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 76189ae449dfd8cf8d7a5bec78b0db5dd65b46b5 parent baa0cb3
2 files changed, +21 −16
  • ios/LChatMac/ChatFeedWebView.swift +12 −11
  • ios/LChatMac/LChatMacApp.swift +9 −5
diff --git a/ios/LChatMac/ChatFeedWebView.swift b/ios/LChatMac/ChatFeedWebView.swift
index 8db1110..e879056 100644
--- a/ios/LChatMac/ChatFeedWebView.swift
+++ b/ios/LChatMac/ChatFeedWebView.swift
@@ -123,21 +123,22 @@ struct ChatFeedWebView: NSViewRepresentable {
input.dispatchEvent(new Event('change', { bubbles: true }));
};
- // Report the web feed's effective light/dark to native so the app chrome (rail,
+ // Report the web feed's appearance CHOICE to native so the app chrome (rail,
// palette, title bar) matches. chat.js sets color-scheme on <html> to 'light',
- // 'dark', or 'light dark' (follow OS); resolve the last via matchMedia. No web
- // change needed — we just read what the page already sets.
- function effectiveDark() {
+ // 'dark', or 'light dark' (follow OS). Crucially we report the mode, never a
+ // resolved boolean: inside a WKWebView, prefers-color-scheme reflects
+ // NSApp.appearance — so resolving 'system' here reads back whatever native
+ // last pinned, and a forced dark could never release to the real OS setting.
+ function effectiveMode() {
var cs = document.documentElement.style.colorScheme || '';
- if (cs.indexOf('dark') !== -1 && cs.indexOf('light') === -1) return true;
- if (cs.indexOf('light') !== -1 && cs.indexOf('dark') === -1) return false;
- return matchMedia('(prefers-color-scheme: dark)').matches;
+ if (cs.indexOf('dark') !== -1 && cs.indexOf('light') === -1) return 'dark';
+ if (cs.indexOf('light') !== -1 && cs.indexOf('dark') === -1) return 'light';
+ return 'system';
}
function reportTheme() {
- try { window.webkit.messageHandlers.lchatTheme.postMessage({ dark: effectiveDark() }); } catch (e) {}
+ try { window.webkit.messageHandlers.lchatTheme.postMessage({ mode: effectiveMode() }); } catch (e) {}
}
new MutationObserver(reportTheme).observe(document.documentElement, { attributes: true, attributeFilter: ['style', 'data-theme'] });
- matchMedia('(prefers-color-scheme: dark)').addEventListener('change', reportTheme);
reportTheme();
})();
"""
@@ -202,8 +203,8 @@ struct ChatFeedWebView: NSViewRepresentable {
case "lchatDrag": moveWindow(message)
case "lchatNotify": showNotification(message)
case "lchatTheme":
- if let body = message.body as? [String: Any], let dark = body["dark"] as? Bool {
- app.setAppearance(dark: dark, for: chat.id)
+ if let body = message.body as? [String: Any], let mode = body["mode"] as? String {
+ app.setAppearance(mode: mode, for: chat.id)
}
default: break
}
diff --git a/ios/LChatMac/LChatMacApp.swift b/ios/LChatMac/LChatMacApp.swift
index 15e5165..219b53c 100644
--- a/ios/LChatMac/LChatMacApp.swift
+++ b/ios/LChatMac/LChatMacApp.swift
@@ -177,16 +177,20 @@ final class MacAppState: ObservableObject {
// Native appearance follows the web feed's light/dark: each webview reports its
// effective mode (setAppearance), and the *selected* instance's mode drives the
// whole app (rail, palette, title bar) so the chrome matches what's on screen.
- private var appearanceDark: [String: Bool] = [:]
+ private var appearanceMode: [String: String] = [:] // "light" | "dark" | "system"
- func setAppearance(dark: Bool, for id: String) {
- appearanceDark[id] = dark
+ func setAppearance(mode: String, for id: String) {
+ appearanceMode[id] = mode
if id == selection { applyAppearance() }
}
private func applyAppearance() {
- guard let sel = selection, let dark = appearanceDark[sel] else { return }
- NSApp.appearance = NSAppearance(named: dark ? .darkAqua : .aqua)
+ guard let sel = selection, let mode = appearanceMode[sel] else { return }
+ // "system" must UNPIN (nil = follow macOS): the webview's
+ // prefers-color-scheme reads NSApp.appearance, so a pinned value here is
+ // what the page's "follow my device" resolves against — pinning dark once
+ // would make system mode stick dark forever.
+ NSApp.appearance = mode == "system" ? nil : NSAppearance(named: mode == "dark" ? .darkAqua : .aqua)
}
}