import { useCallback, useEffect, useRef, useState } from "react"; import { Palette, Check } from "lucide-react"; import { Button } from "@nous-research/ui/ui/components/button"; import { ListItem } from "@nous-research/ui/ui/components/list-item"; import { BottomPickSheet } from "@/components/BottomPickSheet"; import { Typography } from "@/components/NouiTypography"; import { useBelowBreakpoint } from "@/hooks/useBelowBreakpoint"; import { BUILTIN_THEMES, useTheme } from "@/themes"; import type { DashboardTheme, ThemeListEntry } from "@/themes"; import { useI18n } from "@/i18n"; import { cn } from "@/lib/utils"; /** * Compact theme picker mounted next to the language switcher in the header. * Each dropdown row shows a 3-stop swatch (background / midground / warm * glow) so users can preview the palette before committing. User-defined * themes from `~/.hermes/dashboard-themes/*.yaml` use their API-provided * definitions so they show real palette swatches just like built-ins. * * When placed at the bottom of a container (e.g. the sidebar rail), pass * `dropUp` so the menu opens above the trigger instead of clipping below * the viewport. On viewports below the `sm` breakpoint, `dropUp` uses a * bottom sheet portaled to `document.body` so the picker is not clipped by * the sidebar (same idea as a responsive Drawer). */ export function ThemeSwitcher({ dropUp = false }: ThemeSwitcherProps) { const { themeName, availableThemes, setTheme } = useTheme(); const { t } = useI18n(); const [open, setOpen] = useState(false); const wrapperRef = useRef(null); const narrowViewport = useBelowBreakpoint(640); const useMobileSheet = Boolean(dropUp && narrowViewport); const close = useCallback(() => setOpen(false), []); useEffect(() => { if (!open) return; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") close(); }; document.addEventListener("keydown", onKey); return () => document.removeEventListener("keydown", onKey); }, [open, close]); useEffect(() => { if (!open || useMobileSheet) return; const onMouseDown = (e: MouseEvent) => { if ( wrapperRef.current && !wrapperRef.current.contains(e.target as Node) ) { close(); } }; document.addEventListener("mousedown", onMouseDown); return () => document.removeEventListener("mousedown", onMouseDown); }, [open, close, useMobileSheet]); const current = availableThemes.find((th) => th.name === themeName); const label = current?.label ?? themeName; const sheetTitle = t.theme?.title ?? "Theme"; return (
{useMobileSheet && (
)} {open && !useMobileSheet && (
{sheetTitle}
)}
); } function ThemeSwitcherOptions({ availableThemes, close, setTheme, themeName, }: ThemeSwitcherOptionsProps) { return ( <> {availableThemes.map((th) => { const isActive = th.name === themeName; const paletteTheme = BUILTIN_THEMES[th.name] ?? th.definition; return ( { setTheme(th.name); close(); }} role="option" > {paletteTheme ? ( ) : ( )}
{th.label} {th.description && ( {th.description} )}
); })} ); } function ThemeSwatch({ theme }: { theme: DashboardTheme }) { const { background, midground, warmGlow } = theme.palette; return (
); } function PlaceholderSwatch() { return (
); } interface ThemeSwitcherOptionsProps { availableThemes: ThemeListEntry[]; close: () => void; setTheme: (name: string) => void; themeName: string; } interface ThemeSwitcherProps { dropUp?: boolean; }