diff --git a/src/lib/components/Header.svelte b/src/lib/components/Header.svelte index ec209ff..070f411 100644 --- a/src/lib/components/Header.svelte +++ b/src/lib/components/Header.svelte @@ -4,10 +4,14 @@ import Github from "$lib/icons/github.svg?component"; import { label, links } from "$lib/links"; + import ThemeIcon from "./ThemeIcon.svelte"; + let { index, + onToggleTheme, }: { index: boolean; + onToggleTheme: () => void; } = $props(); @@ -63,6 +67,13 @@ Github profile + onToggleTheme()} + type="button" + > + + diff --git a/src/lib/components/ThemeIcon.svelte b/src/lib/components/ThemeIcon.svelte new file mode 100644 index 0000000..02193fe --- /dev/null +++ b/src/lib/components/ThemeIcon.svelte @@ -0,0 +1,27 @@ + + + + + + + + + + + + diff --git a/src/lib/context/theme.ts b/src/lib/context/theme.ts new file mode 100644 index 0000000..058e844 --- /dev/null +++ b/src/lib/context/theme.ts @@ -0,0 +1,8 @@ +import { getContext, setContext } from "svelte"; + +const KEY = "theme"; + +export const setTheme = (theme: () => "dark" | "light") => + setContext(KEY, theme); + +export const getTheme = () => getContext<() => "dark" | "light">(KEY); diff --git a/src/lib/js/theme.ts b/src/lib/js/theme.ts new file mode 100644 index 0000000..ae7dc1e --- /dev/null +++ b/src/lib/js/theme.ts @@ -0,0 +1,12 @@ +const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); + +const storageTheme = localStorage.getItem("theme"); + +const theme = + storageTheme === "dark" || storageTheme === "light" + ? storageTheme + : mediaQuery.matches + ? "dark" + : "light"; + +document.documentElement.classList.toggle("dark", theme === "dark"); diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index cdbd987..22b58f5 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -3,9 +3,13 @@ import "./style.css"; + import { browser } from "$app/environment"; import { onNavigate } from "$app/navigation"; import { page } from "$app/state"; import Header from "$lib/components/Header.svelte"; + import { setTheme } from "$lib/context/theme"; + import themeScript from "$lib/js/theme?url"; + import { MediaQuery } from "svelte/reactivity"; import { fly } from "svelte/transition"; import type { Snapshot } from "./$types"; @@ -36,14 +40,51 @@ // eslint-disable-next-line svelte/@typescript-eslint/no-unnecessary-condition if (scrollElement) scrollElement.scrollTop = scrollPosition; }); + + // Handle theme + const mediaQuery = new MediaQuery("(prefers-color-scheme: dark)"); + + let storageTheme: null | string = $state( + browser ? window.localStorage.getItem("theme") : null, + ); + + let theme: "dark" | "light" = $derived( + storageTheme === "dark" || storageTheme === "light" + ? storageTheme + : mediaQuery.current + ? "dark" + : "light", + ); + + setTheme(() => theme); + + $effect(() => { + if (browser) + document.documentElement.classList.toggle("dark", theme === "dark"); + }); + void (e.key === "theme" && (storageTheme = e.newValue))} +/> + + + + + - + { + const newTheme = theme === "light" ? "dark" : "light"; + storageTheme = newTheme; + window.localStorage.setItem("theme", newTheme); + }} + />