File
Blob: src/client/islands/theme-toggle.tsx
| 1 | /// <reference lib="dom" /> |
| 2 | |
| 3 | import { useEffect, useState } from "react"; |
| 4 | import { Moon, Sun } from "lucide-react"; |
| 5 | |
| 6 | import { highlightThemeHref } from "@/client/highlight-theme"; |
| 7 | import { hydrateIsland } from "@/client/hydrate"; |
| 8 | |
| 9 | export type ThemeToggleProps = Record<string, never>; |
| 10 | |
| 11 | function applyHighlightTheme(theme: "light" | "dark") { |
| 12 | const stylesheet = document.getElementById("hljs-theme"); |
| 13 | if (!(stylesheet instanceof HTMLLinkElement)) { |
| 14 | return; |
| 15 | } |
| 16 | |
| 17 | stylesheet.href = theme === "dark" ? highlightThemeHref.dark : highlightThemeHref.light; |
| 18 | } |
| 19 | |
| 20 | function applyTheme(theme: "light" | "dark") { |
| 21 | document.documentElement.dataset.theme = theme; |
| 22 | document.documentElement.classList.toggle("dark", theme === "dark"); |
| 23 | applyHighlightTheme(theme); |
| 24 | } |
| 25 | |
| 26 | export function ThemeToggleIsland(_props: ThemeToggleProps) { |
| 27 | const [theme, setTheme] = useState<"light" | "dark">("dark"); |
| 28 | |
| 29 | useEffect(() => { |
| 30 | const initial = document.documentElement.classList.contains("dark") ? "dark" : "light"; |
| 31 | setTheme(initial); |
| 32 | applyTheme(initial); |
| 33 | }, []); |
| 34 | |
| 35 | return ( |
| 36 | <button |
| 37 | type="button" |
| 38 | data-theme-toggle |
| 39 | className="flex items-center gap-2 rounded-lg px-2.5 py-1.5 text-zinc-500 transition-colors hover:bg-zinc-100 hover:text-zinc-800 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-500/50 focus-visible:ring-offset-2 focus-visible:ring-offset-canvas" |
| 40 | aria-label={`Toggle theme (current: ${theme === "dark" ? "Dark" : "Light"})`} |
| 41 | onClick={() => { |
| 42 | const nextTheme = theme === "dark" ? "light" : "dark"; |
| 43 | setTheme(nextTheme); |
| 44 | applyTheme(nextTheme); |
| 45 | try { |
| 46 | localStorage.setItem("theme", nextTheme); |
| 47 | } catch {} |
| 48 | }} |
| 49 | > |
| 50 | <Sun className="icon-light h-5 w-5" aria-hidden="true" /> |
| 51 | <Moon className="icon-dark h-5 w-5" aria-hidden="true" /> |
| 52 | </button> |
| 53 | ); |
| 54 | } |
| 55 | |
| 56 | export function initThemeToggle() { |
| 57 | hydrateIsland<ThemeToggleProps>("theme-toggle", ThemeToggleIsland); |
| 58 | } |