feat(ui): light/dark theme toggle and light mode color support

Add ThemeContext with localStorage persistence and FOUC-preventing
inline script. Add theme toggle button in sidebar. Update status
badges, toast notifications, live indicators, and approval cards
with dark: prefixed classes for proper light mode rendering.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Forgotten
2026-02-26 16:33:29 -06:00
parent e2c5b6698c
commit 5cd12dec89
13 changed files with 245 additions and 92 deletions

View File

@@ -0,0 +1,83 @@
import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useState,
type ReactNode,
} from "react";
type Theme = "light" | "dark";
interface ThemeContextValue {
theme: Theme;
setTheme: (theme: Theme) => void;
toggleTheme: () => void;
}
const THEME_STORAGE_KEY = "paperclip.theme";
const DARK_THEME_COLOR = "#18181b";
const LIGHT_THEME_COLOR = "#ffffff";
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
function resolveThemeFromDocument(): Theme {
if (typeof document === "undefined") return "dark";
return document.documentElement.classList.contains("dark") ? "dark" : "light";
}
function applyTheme(theme: Theme) {
if (typeof document === "undefined") return;
const isDark = theme === "dark";
const root = document.documentElement;
root.classList.toggle("dark", isDark);
root.style.colorScheme = isDark ? "dark" : "light";
const themeColorMeta = document.querySelector('meta[name="theme-color"]');
if (themeColorMeta instanceof HTMLMetaElement) {
themeColorMeta.setAttribute("content", isDark ? DARK_THEME_COLOR : LIGHT_THEME_COLOR);
}
}
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setThemeState] = useState<Theme>(() => resolveThemeFromDocument());
const setTheme = useCallback((nextTheme: Theme) => {
setThemeState(nextTheme);
}, []);
const toggleTheme = useCallback(() => {
setThemeState((current) => (current === "dark" ? "light" : "dark"));
}, []);
useEffect(() => {
applyTheme(theme);
try {
localStorage.setItem(THEME_STORAGE_KEY, theme);
} catch {
// Ignore local storage write failures in restricted environments.
}
}, [theme]);
const value = useMemo(
() => ({
theme,
setTheme,
toggleTheme,
}),
[theme, setTheme, toggleTheme],
);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error("useTheme must be used within ThemeProvider");
}
return context;
}