mirror of
https://github.com/luckyyzh/pi-agent-integrated.git
synced 2026-10-03 11:09:34 +00:00
86 lines
2.2 KiB
TypeScript
86 lines
2.2 KiB
TypeScript
"use client";
|
|
|
|
import { useCallback, useSyncExternalStore } from "react";
|
|
|
|
type Theme = "light" | "dark";
|
|
|
|
const listeners = new Set<() => void>();
|
|
|
|
function subscribe(cb: () => void): () => void {
|
|
listeners.add(cb);
|
|
return () => {
|
|
listeners.delete(cb);
|
|
};
|
|
}
|
|
|
|
function getSnapshot(): Theme {
|
|
if (typeof document === "undefined") return "light";
|
|
return document.documentElement.classList.contains("dark") ? "dark" : "light";
|
|
}
|
|
|
|
function getServerSnapshot(): Theme {
|
|
return "light";
|
|
}
|
|
|
|
type ToggleOrigin = { x: number; y: number };
|
|
|
|
export function useTheme() {
|
|
const theme = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
|
|
const toggleTheme = useCallback((origin?: ToggleOrigin) => {
|
|
const next: Theme = getSnapshot() === "dark" ? "light" : "dark";
|
|
|
|
const apply = () => {
|
|
if (next === "dark") {
|
|
document.documentElement.classList.add("dark");
|
|
} else {
|
|
document.documentElement.classList.remove("dark");
|
|
}
|
|
try {
|
|
localStorage.setItem("pi-theme", next);
|
|
} catch {
|
|
// ignore storage errors (private mode, quota, etc.)
|
|
}
|
|
listeners.forEach((cb) => cb());
|
|
};
|
|
|
|
const reduceMotion = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
const supportsVT = typeof document.startViewTransition === "function";
|
|
|
|
if (!supportsVT || reduceMotion) {
|
|
apply();
|
|
return;
|
|
}
|
|
|
|
const x = origin?.x ?? window.innerWidth / 2;
|
|
const y = origin?.y ?? window.innerHeight / 2;
|
|
const endRadius = Math.hypot(
|
|
Math.max(x, window.innerWidth - x),
|
|
Math.max(y, window.innerHeight - y),
|
|
);
|
|
|
|
const transition = document.startViewTransition(apply);
|
|
transition.ready
|
|
.then(() => {
|
|
document.documentElement.animate(
|
|
{
|
|
clipPath: [
|
|
`circle(0px at ${x}px ${y}px)`,
|
|
`circle(${endRadius}px at ${x}px ${y}px)`,
|
|
],
|
|
},
|
|
{
|
|
duration: 450,
|
|
easing: "cubic-bezier(0.22, 0.61, 0.36, 1)",
|
|
pseudoElement: "::view-transition-new(root)",
|
|
},
|
|
);
|
|
})
|
|
.catch(() => {
|
|
// transition cancelled — ignore
|
|
});
|
|
}, []);
|
|
|
|
return { theme, toggleTheme, isDark: theme === "dark" };
|
|
}
|