Add theme subscription and update theme toggle logic
Browse files- src/lib/components/NavMenu.svelte +15 -4
- src/lib/switchTheme.ts +39 -1
src/lib/components/NavMenu.svelte
CHANGED
|
@@ -13,8 +13,9 @@
|
|
| 13 |
import Logo from "$lib/components/icons/Logo.svelte";
|
| 14 |
import IconSun from "$lib/components/icons/IconSun.svelte";
|
| 15 |
import IconMoon from "$lib/components/icons/IconMoon.svelte";
|
| 16 |
-
import { switchTheme } from "$lib/switchTheme";
|
| 17 |
import { isAborted } from "$lib/stores/isAborted";
|
|
|
|
| 18 |
|
| 19 |
import NavConversationItem from "./NavConversationItem.svelte";
|
| 20 |
import type { LayoutData } from "../../routes/$types";
|
|
@@ -100,7 +101,18 @@
|
|
| 100 |
}
|
| 101 |
});
|
| 102 |
|
| 103 |
-
let
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
</script>
|
| 105 |
|
| 106 |
<div
|
|
@@ -188,13 +200,12 @@
|
|
| 188 |
<button
|
| 189 |
onclick={() => {
|
| 190 |
switchTheme();
|
| 191 |
-
theme = localStorage.theme;
|
| 192 |
}}
|
| 193 |
aria-label="Toggle theme"
|
| 194 |
class="flex size-9 min-w-[1.5em] flex-none items-center justify-center rounded-lg p-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
|
| 195 |
>
|
| 196 |
{#if browser}
|
| 197 |
-
{#if
|
| 198 |
<IconSun />
|
| 199 |
{:else}
|
| 200 |
<IconMoon />
|
|
|
|
| 13 |
import Logo from "$lib/components/icons/Logo.svelte";
|
| 14 |
import IconSun from "$lib/components/icons/IconSun.svelte";
|
| 15 |
import IconMoon from "$lib/components/icons/IconMoon.svelte";
|
| 16 |
+
import { switchTheme, subscribeToTheme } from "$lib/switchTheme";
|
| 17 |
import { isAborted } from "$lib/stores/isAborted";
|
| 18 |
+
import { onDestroy } from "svelte";
|
| 19 |
|
| 20 |
import NavConversationItem from "./NavConversationItem.svelte";
|
| 21 |
import type { LayoutData } from "../../routes/$types";
|
|
|
|
| 101 |
}
|
| 102 |
});
|
| 103 |
|
| 104 |
+
let isDark = $state(false);
|
| 105 |
+
let unsubscribeTheme: (() => void) | undefined;
|
| 106 |
+
|
| 107 |
+
if (browser) {
|
| 108 |
+
unsubscribeTheme = subscribeToTheme(({ isDark: nextIsDark }) => {
|
| 109 |
+
isDark = nextIsDark;
|
| 110 |
+
});
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
onDestroy(() => {
|
| 114 |
+
unsubscribeTheme?.();
|
| 115 |
+
});
|
| 116 |
</script>
|
| 117 |
|
| 118 |
<div
|
|
|
|
| 200 |
<button
|
| 201 |
onclick={() => {
|
| 202 |
switchTheme();
|
|
|
|
| 203 |
}}
|
| 204 |
aria-label="Toggle theme"
|
| 205 |
class="flex size-9 min-w-[1.5em] flex-none items-center justify-center rounded-lg p-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
|
| 206 |
>
|
| 207 |
{#if browser}
|
| 208 |
+
{#if isDark}
|
| 209 |
<IconSun />
|
| 210 |
{:else}
|
| 211 |
<IconMoon />
|
src/lib/switchTheme.ts
CHANGED
|
@@ -1,5 +1,37 @@
|
|
| 1 |
export type ThemePreference = "light" | "dark" | "system";
|
| 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
function setMetaThemeColor(isDark: boolean) {
|
| 4 |
const metaTheme = document.querySelector('meta[name="theme-color"]') as HTMLMetaElement | null;
|
| 5 |
if (!metaTheme) return;
|
|
@@ -11,11 +43,16 @@ function applyDarkClass(isDark: boolean) {
|
|
| 11 |
if (isDark) classList.add("dark");
|
| 12 |
else classList.remove("dark");
|
| 13 |
setMetaThemeColor(isDark);
|
|
|
|
| 14 |
}
|
| 15 |
|
| 16 |
export function getThemePreference(): ThemePreference {
|
| 17 |
const raw = typeof localStorage !== "undefined" ? localStorage.getItem("theme") : null;
|
| 18 |
-
if (raw === "light" || raw === "dark" || raw === "system")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
return "system";
|
| 20 |
}
|
| 21 |
|
|
@@ -33,6 +70,7 @@ export function setTheme(preference: ThemePreference) {
|
|
| 33 |
}
|
| 34 |
|
| 35 |
const mql = window.matchMedia("(prefers-color-scheme: dark)");
|
|
|
|
| 36 |
const resolve = () =>
|
| 37 |
applyDarkClass(preference === "dark" || (preference === "system" && mql.matches));
|
| 38 |
|
|
|
|
| 1 |
export type ThemePreference = "light" | "dark" | "system";
|
| 2 |
|
| 3 |
+
type ThemeState = {
|
| 4 |
+
preference: ThemePreference;
|
| 5 |
+
isDark: boolean;
|
| 6 |
+
};
|
| 7 |
+
|
| 8 |
+
type ThemeSubscriber = (state: ThemeState) => void;
|
| 9 |
+
|
| 10 |
+
let currentPreference: ThemePreference = "system";
|
| 11 |
+
const subscribers = new Set<ThemeSubscriber>();
|
| 12 |
+
|
| 13 |
+
function notify(preference: ThemePreference, isDark: boolean) {
|
| 14 |
+
for (const subscriber of subscribers) {
|
| 15 |
+
subscriber({ preference, isDark });
|
| 16 |
+
}
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
export function subscribeToTheme(subscriber: ThemeSubscriber) {
|
| 20 |
+
subscribers.add(subscriber);
|
| 21 |
+
|
| 22 |
+
if (typeof document !== "undefined") {
|
| 23 |
+
const preference = getThemePreference();
|
| 24 |
+
const isDark = document.documentElement.classList.contains("dark");
|
| 25 |
+
subscriber({ preference, isDark });
|
| 26 |
+
} else {
|
| 27 |
+
subscriber({ preference: "system", isDark: false });
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
return () => {
|
| 31 |
+
subscribers.delete(subscriber);
|
| 32 |
+
};
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
function setMetaThemeColor(isDark: boolean) {
|
| 36 |
const metaTheme = document.querySelector('meta[name="theme-color"]') as HTMLMetaElement | null;
|
| 37 |
if (!metaTheme) return;
|
|
|
|
| 43 |
if (isDark) classList.add("dark");
|
| 44 |
else classList.remove("dark");
|
| 45 |
setMetaThemeColor(isDark);
|
| 46 |
+
notify(currentPreference, isDark);
|
| 47 |
}
|
| 48 |
|
| 49 |
export function getThemePreference(): ThemePreference {
|
| 50 |
const raw = typeof localStorage !== "undefined" ? localStorage.getItem("theme") : null;
|
| 51 |
+
if (raw === "light" || raw === "dark" || raw === "system") {
|
| 52 |
+
currentPreference = raw;
|
| 53 |
+
return raw;
|
| 54 |
+
}
|
| 55 |
+
currentPreference = "system";
|
| 56 |
return "system";
|
| 57 |
}
|
| 58 |
|
|
|
|
| 70 |
}
|
| 71 |
|
| 72 |
const mql = window.matchMedia("(prefers-color-scheme: dark)");
|
| 73 |
+
currentPreference = preference;
|
| 74 |
const resolve = () =>
|
| 75 |
applyDarkClass(preference === "dark" || (preference === "system" && mql.matches));
|
| 76 |
|