mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-05-17 02:09:09 +08:00
feat: circular reveal animation for theme switching (#905)
This commit is contained in:
@@ -7,13 +7,13 @@ export function ModeToggle() {
|
||||
const { theme, setTheme } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const toggleTheme = () => {
|
||||
const toggleTheme = (event: React.MouseEvent) => {
|
||||
// 如果当前是 dark 或 system(且系统是暗色),切换到 light
|
||||
// 否则切换到 dark
|
||||
if (theme === "dark") {
|
||||
setTheme("light");
|
||||
setTheme("light", event);
|
||||
} else {
|
||||
setTheme("dark");
|
||||
setTheme("dark", event);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -19,21 +19,21 @@ export function ThemeSettings() {
|
||||
<div className="inline-flex gap-1 rounded-md border border-border-default bg-background p-1">
|
||||
<ThemeButton
|
||||
active={theme === "light"}
|
||||
onClick={() => setTheme("light")}
|
||||
onClick={(e) => setTheme("light", e)}
|
||||
icon={Sun}
|
||||
>
|
||||
{t("settings.themeLight")}
|
||||
</ThemeButton>
|
||||
<ThemeButton
|
||||
active={theme === "dark"}
|
||||
onClick={() => setTheme("dark")}
|
||||
onClick={(e) => setTheme("dark", e)}
|
||||
icon={Moon}
|
||||
>
|
||||
{t("settings.themeDark")}
|
||||
</ThemeButton>
|
||||
<ThemeButton
|
||||
active={theme === "system"}
|
||||
onClick={() => setTheme("system")}
|
||||
onClick={(e) => setTheme("system", e)}
|
||||
icon={Monitor}
|
||||
>
|
||||
{t("settings.themeSystem")}
|
||||
@@ -45,7 +45,7 @@ export function ThemeSettings() {
|
||||
|
||||
interface ThemeButtonProps {
|
||||
active: boolean;
|
||||
onClick: () => void;
|
||||
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ interface ThemeProviderProps {
|
||||
|
||||
interface ThemeContextValue {
|
||||
theme: Theme;
|
||||
setTheme: (theme: Theme) => void;
|
||||
setTheme: (theme: Theme, event?: React.MouseEvent) => void;
|
||||
}
|
||||
|
||||
const ThemeProviderContext = createContext<ThemeContextValue | undefined>(
|
||||
@@ -146,8 +146,30 @@ export function ThemeProvider({
|
||||
const value = useMemo<ThemeContextValue>(
|
||||
() => ({
|
||||
theme,
|
||||
setTheme: (nextTheme: Theme) => {
|
||||
setThemeState(nextTheme);
|
||||
setTheme: (nextTheme: Theme, event?: React.MouseEvent) => {
|
||||
// Skip if same theme
|
||||
if (nextTheme === theme) return;
|
||||
|
||||
// Set transition origin coordinates from click event
|
||||
const x = event?.clientX ?? window.innerWidth / 2;
|
||||
const y = event?.clientY ?? window.innerHeight / 2;
|
||||
document.documentElement.style.setProperty(
|
||||
"--theme-transition-x",
|
||||
`${x}px`,
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
"--theme-transition-y",
|
||||
`${y}px`,
|
||||
);
|
||||
|
||||
// Use View Transitions API if available, otherwise fall back to instant change
|
||||
if (document.startViewTransition) {
|
||||
document.startViewTransition(() => {
|
||||
setThemeState(nextTheme);
|
||||
});
|
||||
} else {
|
||||
setThemeState(nextTheme);
|
||||
}
|
||||
},
|
||||
}),
|
||||
[theme],
|
||||
|
||||
@@ -215,3 +215,42 @@ input[type="password"]::-ms-reveal,
|
||||
input[type="password"]::-ms-clear {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Theme transition animation using View Transitions API */
|
||||
::view-transition-old(root),
|
||||
::view-transition-new(root) {
|
||||
animation: none;
|
||||
mix-blend-mode: normal;
|
||||
}
|
||||
|
||||
/* Old snapshot stays behind, new snapshot animates on top */
|
||||
::view-transition-old(root) {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
::view-transition-new(root) {
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
/* Circular expand animation from click position */
|
||||
@keyframes theme-circle-expand {
|
||||
from {
|
||||
clip-path: circle(0% at var(--theme-transition-x, 50%) var(--theme-transition-y, 50%));
|
||||
}
|
||||
|
||||
to {
|
||||
clip-path: circle(150% at var(--theme-transition-x, 50%) var(--theme-transition-y, 50%));
|
||||
}
|
||||
}
|
||||
|
||||
/* Apply animation to new snapshot - works for both light and dark transitions */
|
||||
::view-transition-new(root) {
|
||||
animation: theme-circle-expand 0.4s ease-out;
|
||||
}
|
||||
|
||||
/* Respect user preference for reduced motion */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
::view-transition-new(root) {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user