"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 LightPalette = "aged-parchment" | "tulasi-garden" | "yamuna-mist"; type DarkPalette = "ancient-manuscript" | "nilamani" | "vraja-twilight"; type ReadingPrefs = { serifFont: boolean; fontSize: string; justified: boolean; lightPalette: LightPalette; darkPalette: DarkPalette; }; const DEFAULT_PREFS: ReadingPrefs = { serifFont: false, fontSize: "base", justified: false, lightPalette: "aged-parchment", darkPalette: "ancient-manuscript", }; // Preview swatches use approximations of each palette's background // and primary colours — the real colours come from the CSS variables. const LIGHT_PALETTES: { id: LightPalette; name: string; bg: string; primary: string; }[] = [ // Warm cream + saffron — the original { id: "aged-parchment", name: "Aged Parchment", bg: "#f5ecd7", primary: "#b88940", }, // Sage green background + deep emerald — distinctly GREEN { id: "tulasi-garden", name: "Tulasī Garden", bg: "#c4dcc1", primary: "#207537", }, // Soft blue background + cerulean — distinctly BLUE { id: "yamuna-mist", name: "Yamunā Mist", bg: "#c2d7ec", primary: "#1f6fd6", }, ]; const DARK_PALETTES: { id: DarkPalette; name: string; bg: string; primary: string; }[] = [ // Warm brown + saffron — the original { id: "ancient-manuscript", name: "Ancient Manuscript", bg: "#2a2519", primary: "#d0a04e", }, // Deep navy + sky-blue — distinctly BLUE { id: "nilamani", name: "Nīlamaṇi", bg: "#101a36", primary: "#4a9fff", }, // Deep wine + rose-red — distinctly RED/WARM { id: "vraja-twilight", name: "Vraja Twilight", bg: "#2b1217", primary: "#e07252", }, ]; const LIGHT_PALETTE_IDS = LIGHT_PALETTES.map((p) => p.id); const DARK_PALETTE_IDS = DARK_PALETTES.map((p) => p.id); 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, lightPalette: LIGHT_PALETTE_IDS.includes(parsed.lightPalette) ? parsed.lightPalette : DEFAULT_PREFS.lightPalette, darkPalette: DARK_PALETTE_IDS.includes(parsed.darkPalette) ? parsed.darkPalette : DEFAULT_PREFS.darkPalette, }; } 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); // Palettes — always set both attributes so the appropriate one takes // effect when the user toggles light ↔ dark. root.setAttribute("data-light-palette", prefs.lightPalette); root.setAttribute("data-dark-palette", prefs.darkPalette); } 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, resolvedTheme } = 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"; // Show palettes matching the currently visible mode. Falls back to light if // resolvedTheme hasn't been determined yet (e.g. first render). const isDark = resolvedTheme === "dark"; const visiblePalettes = isDark ? DARK_PALETTES : LIGHT_PALETTES; const activePaletteId = isDark ? prefs.darkPalette : prefs.lightPalette; const activePaletteName = visiblePalettes.find((p) => p.id === activePaletteId)?.name ?? ""; 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 }) => ( ))}
{/* Palette selector — shows light or dark palettes depending on the currently resolved theme. */}
Palette {activePaletteName}
{visiblePalettes.map((p) => { const isActive = activePaletteId === p.id; return ( ); })}
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
)}
); }