feat: integrate Pi backend and Pi Web

This commit is contained in:
luckyyzh
2026-07-30 19:37:53 +08:00
commit 7392ab9dd7
1390 changed files with 337197 additions and 0 deletions
+85
View File
@@ -0,0 +1,85 @@
"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" };
}