tfrere's picture
tfrere HF Staff
feat(frontend): editor refresh (embed studio, comment popover, shiki, top bar, hooks, styles)
76fc93a
Raw
History Blame Contribute Delete
1.86 kB
import { useState, useEffect } from "react";
/**
* Shared singleton observer for data-theme + --primary-color.
* Only one MutationObserver runs regardless of how many components subscribe.
*/
type ThemeListener = (isDark: boolean, primaryColor: string) => void;
const listeners = new Set<ThemeListener>();
let observer: MutationObserver | null = null;
let cachedDark = document.documentElement.getAttribute("data-theme") === "dark";
let cachedColor =
getComputedStyle(document.documentElement)
.getPropertyValue("--primary-color")
.trim() || "#4e79a7";
function notify() {
const dark = document.documentElement.getAttribute("data-theme") === "dark";
const color =
getComputedStyle(document.documentElement)
.getPropertyValue("--primary-color")
.trim() || "#4e79a7";
const changed = dark !== cachedDark || color !== cachedColor;
cachedDark = dark;
cachedColor = color;
if (changed) {
for (const fn of listeners) fn(dark, color);
}
}
function subscribe(fn: ThemeListener): () => void {
listeners.add(fn);
if (!observer) {
observer = new MutationObserver(notify);
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["data-theme", "style"],
});
}
return () => {
listeners.delete(fn);
if (listeners.size === 0 && observer) {
observer.disconnect();
observer = null;
}
};
}
export function useTheme(): { isDark: boolean; primaryColor: string } {
const [isDark, setIsDark] = useState(cachedDark);
const [primaryColor, setPrimaryColor] = useState(cachedColor);
useEffect(() => {
return subscribe((dark, color) => {
setIsDark(dark);
setPrimaryColor(color);
});
}, []);
return { isDark, primaryColor };
}
export function useIsDark(): boolean {
const { isDark } = useTheme();
return isDark;
}