A very simple bookmarking webapp bookmarker.finxol.deno.net/

feat: add theme switcher

finxol.io aafcd9dd aa0f504e

verified
+91 -19
+62
src/components/ThemeSwitcher.tsx
··· 1 + import { createEffect, createSignal, onCleanup, onMount } from "solid-js" 2 + import { MoonIcon, SunIcon } from "lucide-solid" 3 + 4 + type Theme = "light" | "dark" 5 + 6 + const STORAGE_KEY = "theme" 7 + 8 + function getSystemTheme(): Theme { 9 + return window.matchMedia("(prefers-color-scheme: dark)").matches 10 + ? "dark" 11 + : "light" 12 + } 13 + 14 + function getInitialTheme(): Theme { 15 + const stored = localStorage.getItem(STORAGE_KEY) 16 + if (stored === "light" || stored === "dark") { 17 + return stored 18 + } 19 + return getSystemTheme() 20 + } 21 + 22 + export function ThemeSwitcher() { 23 + const [theme, setTheme] = createSignal<Theme>(getInitialTheme()) 24 + 25 + // Reactively apply the theme to <html> and persist to localStorage 26 + createEffect(() => { 27 + const current = theme() 28 + document.documentElement.dataset.theme = current 29 + localStorage.setItem(STORAGE_KEY, current) 30 + }) 31 + 32 + // Listen for system preference changes when no explicit choice is stored 33 + onMount(() => { 34 + const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)") 35 + 36 + const handleChange = (e: MediaQueryListEvent) => { 37 + if (!localStorage.getItem(STORAGE_KEY)) { 38 + setTheme(e.matches ? "dark" : "light") 39 + } 40 + } 41 + 42 + mediaQuery.addEventListener("change", handleChange) 43 + onCleanup(() => mediaQuery.removeEventListener("change", handleChange)) 44 + }) 45 + 46 + const toggleTheme = () => { 47 + setTheme((prev) => (prev === "light" ? "dark" : "light")) 48 + } 49 + 50 + return ( 51 + <button 52 + type="button" 53 + onClick={toggleTheme} 54 + title={`Switch to ${theme() === "light" ? "dark" : "light"} mode`} 55 + class="button-icon button-ghost" 56 + > 57 + {theme() === "light" 58 + ? <MoonIcon size={24} /> 59 + : <SunIcon size={24} />} 60 + </button> 61 + ) 62 + }
+15 -11
src/routes/__root.tsx
··· 1 1 import { createRootRoute, Outlet } from "@tanstack/solid-router" 2 2 import { useQuery } from "@tanstack/solid-query" 3 + import { Link } from "@tanstack/solid-router" 4 + import { BookmarkIcon, LogInIcon } from "lucide-solid" 5 + import { Show } from "solid-js" 3 6 import { client } from "../apiclient.ts" 4 - import { Link } from "@tanstack/solid-router" 7 + import { ThemeSwitcher } from "../components/ThemeSwitcher.tsx" 5 8 import "./main.css" 6 9 import "./root.css" 7 - import { BookmarkIcon, LogInIcon } from "lucide-solid" 8 - import { Show } from "solid-js" 9 10 10 11 export const Route = createRootRoute({ 11 12 component: RootComponent, ··· 35 36 </Link> 36 37 </h1> 37 38 </nav> 38 - <Show when={!query.isPending && query.data}> 39 - <Link to="/account"> 40 - <img 41 - src={query.data?.avatar ?? undefined} 42 - alt={query.data?.name} 43 - /> 44 - </Link> 45 - </Show> 39 + <div> 40 + <ThemeSwitcher /> 41 + <Show when={!query.isPending && query.data}> 42 + <Link to="/account"> 43 + <img 44 + src={query.data?.avatar ?? undefined} 45 + alt={query.data?.name} 46 + /> 47 + </Link> 48 + </Show> 49 + </div> 46 50 </header> 47 51 {!query.isPending && 48 52 (query.data ? <Outlet /> : <LoggedOutPage />)}
+14 -8
src/routes/root.css
··· 44 44 } 45 45 } 46 46 47 - img { 48 - --size: 2.25rem; 49 - border-radius: 50%; 50 - object-fit: cover; 51 - border: 2px solid oklch(from var(--primary) l c h / 0.2); 52 - transition: border-color 0.15s ease; 47 + & > div { 48 + display: flex; 49 + align-items: center; 50 + gap: var(--spacing); 53 51 54 - &:hover { 55 - border-color: var(--primary); 52 + img { 53 + --size: 2.25rem; 54 + border-radius: 50%; 55 + object-fit: cover; 56 + border: 2px solid oklch(from var(--primary) l c h / 0.2); 57 + transition: border-color 0.15s ease; 58 + 59 + &:hover { 60 + border-color: var(--primary); 61 + } 56 62 } 57 63 } 58 64 }