mirror of
https://github.com/mostlygeek/llama-swap.git
synced 2026-06-09 06:46:34 +02:00
ui: add auto theme switch mode based on system theme (#741)
UI Tests / run-tests (push) Successful in 51s
UI Tests / run-tests (push) Successful in 51s
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:
@@ -9,7 +9,7 @@
|
||||
import Playground from "./routes/Playground.svelte";
|
||||
import PlaygroundStub from "./routes/PlaygroundStub.svelte";
|
||||
import { enableAPIEvents } from "./stores/api";
|
||||
import { initScreenWidth, isDarkMode, appTitle, connectionState } from "./stores/theme";
|
||||
import { initScreenWidth, initSystemThemeListener, isDarkMode, appTitle, connectionState } from "./stores/theme";
|
||||
import { currentRoute } from "./stores/route";
|
||||
|
||||
const routes = {
|
||||
@@ -37,10 +37,12 @@
|
||||
|
||||
onMount(() => {
|
||||
const cleanupScreenWidth = initScreenWidth();
|
||||
const cleanupSystemTheme = initSystemThemeListener();
|
||||
enableAPIEvents(true);
|
||||
|
||||
return () => {
|
||||
cleanupScreenWidth();
|
||||
cleanupSystemTheme();
|
||||
enableAPIEvents(false);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { link } from "svelte-spa-router";
|
||||
import { screenWidth, toggleTheme, isDarkMode, appTitle, isNarrow } from "../stores/theme";
|
||||
import { screenWidth, toggleTheme, themeMode, appTitle, isNarrow } from "../stores/theme";
|
||||
import { currentRoute } from "../stores/route";
|
||||
import { playgroundActivity } from "../stores/playgroundActivity";
|
||||
import ConnectionStatus from "./ConnectionStatus.svelte";
|
||||
@@ -94,19 +94,24 @@
|
||||
>
|
||||
Performance
|
||||
</a>
|
||||
<button onclick={toggleTheme} title="Toggle theme">
|
||||
{#if $isDarkMode}
|
||||
<button onclick={toggleTheme} title="Toggle theme (current: {$themeMode})">
|
||||
{#if $themeMode === "system"}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-5 h-5">
|
||||
<path d="M0,9c0-.552,.448-1,1-1H3.108c.147-.874,.472-1.721,1.006-2.471l-1.478-1.478c-.391-.391-.391-1.023,0-1.414s1.023-.391,1.414,0l1.478,1.478c.751-.534,1.598-.859,2.471-1.006V1c0-.552,.448-1,1-1s1,.448,1,1V3.108c.874,.147,1.725,.466,2.477,1.001l1.473-1.473c.391-.391,1.023-.391,1.414,0s.391,1.023,0,1.414L3.963,15.45c-.195,.195-.451,.293-.707,.293s-.512-.098-.707-.293c-.391-.391-.391-1.023,0-1.414l1.56-1.56c-.535-.751-.854-1.602-1.001-2.477H1c-.552,0-1-.448-1-1ZM23.707,.293c-.391-.391-1.023-.391-1.414,0L.293,22.293c-.391,.391-.391,1.023,0,1.414,.195,.195,.451,.293,.707,.293s.512-.098,.707-.293L23.707,1.707c.391-.391,.391-1.023,0-1.414Zm-.283,10.954c.32-.15,.538-.458,.572-.81,.034-.353-.121-.696-.407-.904-.858-.625-1.833-1.066-2.897-1.315-.335-.078-.69,.022-.934,.267l-8.392,8.391c-.244,.244-.345,.597-.267,.933,.843,3.646,4.047,6.191,7.792,6.191,1.695,0,3.32-.53,4.697-1.533,.286-.208,.441-.553,.407-.904-.034-.353-.251-.66-.572-.811-1.842-.861-3.033-2.727-3.033-4.752s1.19-3.891,3.033-4.753Z"/>
|
||||
</svg>
|
||||
{:else if $themeMode === "light"}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-5 h-5">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M9.528 1.718a.75.75 0 0 1 .162.819A8.97 8.97 0 0 0 9 6a9 9 0 0 0 9 9 8.97 8.97 0 0 0 3.463-.69.75.75 0 0 1 .981.98 10.503 10.503 0 0 1-9.694 6.46c-5.799 0-10.5-4.7-10.5-10.5 0-4.368 2.667-8.112 6.46-9.694a.75.75 0 0 1 .818.162Z"
|
||||
clip-rule="evenodd"
|
||||
d="M12 2.25a.75.75 0 0 1 .75.75v2.25a.75.75 0 0 1-1.5 0V3a.75.75 0 0 1 .75-.75ZM7.5 12a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM18.894 6.166a.75.75 0 0 0-1.06-1.06l-1.591 1.59a.75.75 0 1 0 1.06 1.061l1.591-1.59ZM21.75 12a.75.75 0 0 1-.75.75h-2.25a.75.75 0 0 1 0-1.5H21a.75.75 0 0 1 .75.75ZM17.834 18.894a.75.75 0 0 0 1.06-1.06l-1.59-1.591a.75.75 0 1 0-1.061 1.06l1.591 1.591ZM12 18a.75.75 0 0 1 .75.75V21a.75.75 0 0 1-1.5 0v-2.25A.75.75 0 0 1 12 18ZM7.758 17.303a.75.75 0 0 0-1.061-1.06l-1.591 1.59a.75.75 0 0 0 1.06 1.061l1.591-1.59ZM6 12a.75.75 0 0 1-.75.75H3a.75.75 0 0 1 0-1.5h2.25A.75.75 0 0 1 6 12ZM6.697 7.757a.75.75 0 0 0 1.06-1.06l-1.59-1.591a.75.75 0 0 0-1.061 1.06l1.59 1.591Z"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-5 h-5">
|
||||
<path
|
||||
d="M12 2.25a.75.75 0 0 1 .75.75v2.25a.75.75 0 0 1-1.5 0V3a.75.75 0 0 1 .75-.75ZM7.5 12a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM18.894 6.166a.75.75 0 0 0-1.06-1.06l-1.591 1.59a.75.75 0 1 0 1.06 1.061l1.591-1.59ZM21.75 12a.75.75 0 0 1-.75.75h-2.25a.75.75 0 0 1 0-1.5H21a.75.75 0 0 1 .75.75ZM17.834 18.894a.75.75 0 0 0 1.06-1.06l-1.59-1.591a.75.75 0 1 0-1.061 1.06l1.591 1.591ZM12 18a.75.75 0 0 1 .75.75V21a.75.75 0 0 1-1.5 0v-2.25A.75.75 0 0 1 12 18ZM7.758 17.303a.75.75 0 0 0-1.061-1.06l-1.591 1.59a.75.75 0 0 0 1.06 1.061l1.591-1.59ZM6 12a.75.75 0 0 1-.75.75H3a.75.75 0 0 1 0-1.5h2.25A.75.75 0 0 1 6 12ZM6.697 7.757a.75.75 0 0 0 1.06-1.06l-1.59-1.591a.75.75 0 0 0-1.061 1.06l1.59 1.591Z"
|
||||
fill-rule="evenodd"
|
||||
d="M9.528 1.718a.75.75 0 0 1 .162.819A8.97 8.97 0 0 0 9 6a9 9 0 0 0 9 9 8.97 8.97 0 0 0 3.463-.69.75.75 0 0 1 .981.98 10.503 10.503 0 0 1-9.694 6.46c-5.799 0-10.5-4.7-10.5-10.5 0-4.368 2.667-8.112 6.46-9.694a.75.75 0 0 1 .818.162Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user