ui: add auto theme switch mode based on system theme (#741)

Add system theme detection with automatic switching when OS theme
changes.

- Add ThemeMode type with "light", "dark", and "system" options
- Add system theme listener using matchMedia API
- Update theme toggle to cycle through System → Light → Dark
- Add combined sun/moon icon for system theme mode
- Migrate existing theme preferences to new format
This commit is contained in:
bankjaneo
2026-05-10 10:22:18 +07:00
committed by GitHub
parent 7e3e94a08a
commit 2be3416baa
3 changed files with 76 additions and 10 deletions
+62 -3
View File
@@ -2,11 +2,50 @@ import { writable, derived } from "svelte/store";
import { persistentStore } from "./persistent";
import type { ScreenWidth } from "../lib/types";
export type ThemeMode = "light" | "dark" | "system";
function getInitialThemeMode(): ThemeMode {
if (typeof window !== "undefined") {
try {
const saved = localStorage.getItem("theme");
if (saved !== null) {
const oldTheme = JSON.parse(saved);
localStorage.removeItem("theme");
return oldTheme ? "dark" : "light";
}
} catch (e) {
console.error("Error parsing stored theme", e);
}
}
return "system";
}
// Persistent stores
const systemDark = typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches;
export const isDarkMode = persistentStore<boolean>("theme", systemDark);
export const themeMode = persistentStore<ThemeMode>("theme-mode", getInitialThemeMode());
export const appTitle = persistentStore<string>("app-title", "llama-swap");
const prefersDarkQuery = "(prefers-color-scheme: dark)";
function getSystemPrefersDark(): boolean {
return (
typeof window !== "undefined" &&
typeof window.matchMedia === "function" &&
window.matchMedia(prefersDarkQuery).matches
);
}
// Internal store for the raw OS dark preference
const systemPrefersDark = writable(getSystemPrefersDark());
// Derived store for actual dark mode state
export const isDarkMode = derived(
[themeMode, systemPrefersDark],
([$themeMode, $systemPrefersDark]) => {
if ($themeMode === "system") return $systemPrefersDark;
return $themeMode === "dark";
}
);
// Non-persistent stores
export const screenWidth = writable<ScreenWidth>("md");
export const connectionState = writable<"connected" | "connecting" | "disconnected">("disconnected");
@@ -18,9 +57,15 @@ export const isNarrow = derived(screenWidth, ($screenWidth) => {
// Function to toggle theme
export function toggleTheme(): void {
isDarkMode.update((current) => !current);
themeMode.update((current) => {
if (current === "system") return "light";
if (current === "light") return "dark";
return "system";
});
}
// Function to check and update screen width
export function checkScreenWidth(): void {
const innerWidth = window.innerWidth;
@@ -52,3 +97,17 @@ export function initScreenWidth(): () => void {
window.removeEventListener("resize", checkScreenWidth);
};
}
// Initialize system theme listener
export function initSystemThemeListener(): () => void {
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return () => {};
const mediaQuery = window.matchMedia(prefersDarkQuery);
systemPrefersDark.set(mediaQuery.matches);
const handleChange = (e: MediaQueryListEvent) => {
systemPrefersDark.set(e.matches);
};
mediaQuery.addEventListener("change", handleChange);
return () => mediaQuery.removeEventListener("change", handleChange);
}