"use client"; import { useState, useRef, useEffect, useCallback } from "react"; import { useTheme } from "next-themes"; import { Settings, Sun, Moon, Monitor } from "lucide-react"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import { useSyncExternalStore } from "react"; const emptySubscribe = () => () => {}; function useMounted() { return useSyncExternalStore( emptySubscribe, () => true, () => false, ); } type ReadingPrefs = { serifFont: boolean; fontSize: string; justified: boolean; }; const DEFAULT_PREFS: ReadingPrefs = { serifFont: false, fontSize: "base", justified: false, }; const FONT_SIZES = [ { value: "xs", label: "XS" }, { value: "sm", label: "S" }, { value: "base", label: "M" }, { value: "lg", label: "L" }, { value: "xl", label: "XL" }, { value: "2xl", label: "XXL" }, ] as const; const STORAGE_KEY = "spsays_reading_prefs"; function loadPrefs(): ReadingPrefs { if (typeof window === "undefined") return DEFAULT_PREFS; try { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) return DEFAULT_PREFS; const parsed = JSON.parse(raw); return { serifFont: typeof parsed.serifFont === "boolean" ? parsed.serifFont : DEFAULT_PREFS.serifFont, fontSize: FONT_SIZES.some((f) => f.value === parsed.fontSize) ? parsed.fontSize : DEFAULT_PREFS.fontSize, justified: typeof parsed.justified === "boolean" ? parsed.justified : DEFAULT_PREFS.justified, }; } catch { return DEFAULT_PREFS; } } function savePrefs(prefs: ReadingPrefs) { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(prefs)); } catch { // localStorage might be unavailable } } function applyClasses(prefs: ReadingPrefs) { const root = document.documentElement; // Serif root.classList.toggle("reading-serif", prefs.serifFont); // Font size — remove all, add current for (const fs of FONT_SIZES) { root.classList.remove(`reading-fs-${fs.value}`); } root.classList.add(`reading-fs-${prefs.fontSize}`); // Justified root.classList.toggle("reading-justified", prefs.justified); } function Toggle({ checked, onChange, label, }: { checked: boolean; onChange: (v: boolean) => void; label: string; }) { return ( ); } export function ReadingSettings() { const [open, setOpen] = useState(false); const [prefs, setPrefs] = useState(() => loadPrefs()); const panelRef = useRef(null); const buttonRef = useRef(null); const { theme, setTheme } = useTheme(); const mounted = useMounted(); // Apply classes on mount (prefs already loaded via lazy initializer) useEffect(() => { applyClasses(prefs); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Click outside to close useEffect(() => { if (!open) return; function handleClickOutside(e: MouseEvent) { if ( panelRef.current && !panelRef.current.contains(e.target as Node) && buttonRef.current && !buttonRef.current.contains(e.target as Node) ) { setOpen(false); } } function handleEscape(e: KeyboardEvent) { if (e.key === "Escape") { setOpen(false); } } document.addEventListener("mousedown", handleClickOutside); document.addEventListener("keydown", handleEscape); return () => { document.removeEventListener("mousedown", handleClickOutside); document.removeEventListener("keydown", handleEscape); }; }, [open]); const updatePrefs = useCallback((update: Partial) => { setPrefs((prev) => { const next = { ...prev, ...update }; savePrefs(next); applyClasses(next); return next; }); }, []); const fontSizeIndex = FONT_SIZES.findIndex((f) => f.value === prefs.fontSize); const currentFontSizeLabel = FONT_SIZES[fontSizeIndex]?.label ?? "M"; if (!mounted) { return ( ); } return (
{open && (
{/* Theme selector */}
Theme
{( [ { value: "light", icon: Sun, label: "Light" }, { value: "dark", icon: Moon, label: "Dark" }, { value: "system", icon: Monitor, label: "System" }, ] as const ).map(({ value, icon: Icon, label }) => ( ))}
Reading {/* Serif font toggle */} updatePrefs({ serifFont: v })} label="Serif Font" /> {/* Justify text toggle */} updatePrefs({ justified: v })} label="Justify Text" /> {/* Font size slider */}
Font Size {currentFontSizeLabel}
A { const idx = parseInt(e.target.value, 10); updatePrefs({ fontSize: FONT_SIZES[idx].value }); }} className="flex-1 h-1.5 accent-primary cursor-pointer" /> A
)}
); }