diff --git a/ui/src/components/SidebarUserMenu.tsx b/ui/src/components/SidebarUserMenu.tsx index c04f7645..433b5f39 100644 --- a/ui/src/components/SidebarUserMenu.tsx +++ b/ui/src/components/SidebarUserMenu.tsx @@ -1,10 +1,24 @@ import { Link, useNavigate } from "@tanstack/react-router"; import { useQueryClient } from "@tanstack/react-query"; import { useEffect, useRef, useState } from "react"; -import { IoLogOutOutline, IoSettingsOutline } from "react-icons/io5"; +import { + IoDesktopOutline, + IoLogOutOutline, + IoMoonOutline, + IoSettingsOutline, + IoSunnyOutline, +} from "react-icons/io5"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { api, type SessionUser } from "@/lib/api"; +import { useTheme, type Theme } from "@/lib/theme"; +import { cn } from "@/lib/utils"; + +const THEME_OPTIONS: Array<{ value: Theme; label: string; icon: typeof IoSunnyOutline }> = [ + { value: "light", label: "Light", icon: IoSunnyOutline }, + { value: "dark", label: "Dark", icon: IoMoonOutline }, + { value: "system", label: "System", icon: IoDesktopOutline }, +]; interface SidebarUserMenuProps { user: SessionUser; @@ -14,6 +28,7 @@ interface SidebarUserMenuProps { export function SidebarUserMenu({ user, showSettingsLink = false }: SidebarUserMenuProps) { const qc = useQueryClient(); const navigate = useNavigate(); + const { theme, setTheme } = useTheme(); const [open, setOpen] = useState(false); const ref = useRef(null); @@ -67,6 +82,35 @@ export function SidebarUserMenu({ user, showSettingsLink = false }: SidebarUserM role="menu" className="absolute right-0 bottom-full left-0 mb-2 overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md" > +
+ + Theme + +
+ {THEME_OPTIONS.map((option) => { + const Icon = option.icon; + const active = theme === option.value; + return ( + + ); + })} +
+
+
{showSettingsLink && ( ("system") + const [resolvedTheme, setResolvedTheme] = useState("light") + const themeRef = useRef("system") + + useEffect(() => { + const stored = readStoredTheme() + themeRef.current = stored + setThemeState(stored) + setResolvedTheme(resolveTheme(stored)) + applyTheme(resolveTheme(stored)) + + const media = window.matchMedia("(prefers-color-scheme: dark)") + const onChange = () => { + if (themeRef.current !== "system") return + const next = systemPrefersDark() ? "dark" : "light" + setResolvedTheme(next) + applyTheme(next) + } + media.addEventListener("change", onChange) + return () => media.removeEventListener("change", onChange) + }, []) + + const setTheme = useCallback((next: Theme) => { + if (typeof window !== "undefined") { + window.localStorage.setItem(THEME_STORAGE_KEY, next) + } + themeRef.current = next + const resolved = resolveTheme(next) + setThemeState(next) + setResolvedTheme(resolved) + applyTheme(resolved) + }, []) + + const toggleTheme = useCallback(() => { + setTheme(resolvedTheme === "dark" ? "light" : "dark") + }, [resolvedTheme, setTheme]) + + return { theme, resolvedTheme, setTheme, toggleTheme } +} diff --git a/ui/src/routes/__root.tsx b/ui/src/routes/__root.tsx index 92205f66..de488a8f 100644 --- a/ui/src/routes/__root.tsx +++ b/ui/src/routes/__root.tsx @@ -13,6 +13,8 @@ import { useState } from "react" import appCss from "../styles.css?url" import { makeQueryClient } from "@/lib/query" +const themeInitScript = `(function(){try{var t=localStorage.getItem("open-swe-theme");var d=t==="dark"||((!t||t==="system")&&window.matchMedia("(prefers-color-scheme: dark)").matches);var r=document.documentElement;r.classList.toggle("dark",d);r.style.colorScheme=d?"dark":"light";}catch(e){}})();` + export const Route = createRootRoute({ head: () => ({ meta: [ @@ -47,6 +49,7 @@ function RootDocument({ children }: { children: React.ReactNode }) { return ( +