fix(ui): restore missing UI transition animations (#408)

- Wrap `Routes` with `AnimatePresence` to enable framer-motion page transition exit animations and fix harsh route changes.
- Replace `tw-animate-css` with `tailwindcss-animate` to properly bundle transition classes (like `animate-in`, `animate-accordion-down`) under Tailwind v4.
- This restores animation to modals, accordions, and all page navigations.
- Fix accessibility warning on table cell span element.

Fixes #283
This commit is contained in:
sagit
2026-04-03 20:55:43 +08:00
committed by GitHub
parent 4e088afb29
commit 7b9b59644e
4 changed files with 118 additions and 111 deletions
+1 -1
View File
@@ -52,7 +52,7 @@
"tailwind-merge": "^2.5.5", "tailwind-merge": "^2.5.5",
"tailwind-variants": "1.0.0", "tailwind-variants": "1.0.0",
"tailwindcss": "4.1.11", "tailwindcss": "4.1.11",
"tw-animate-css": "^1.3.0" "tailwindcss-animate": "^1.0.7"
}, },
"devDependencies": { "devDependencies": {
"@eslint/compat": "1.2.8", "@eslint/compat": "1.2.8",
+106 -101
View File
@@ -1,5 +1,6 @@
import { Route, Routes, useNavigate } from "react-router-dom"; import { Route, Routes, useLocation, useNavigate } from "react-router-dom";
import { useEffect } from "react"; import { useEffect } from "react";
import { AnimatePresence } from "framer-motion";
import IndexPage from "@/pages/index"; import IndexPage from "@/pages/index";
import ChangePasswordPage from "@/pages/change-password"; import ChangePasswordPage from "@/pages/change-password";
@@ -87,6 +88,8 @@ const LoginRoute = () => {
}; };
function App() { function App() {
const location = useLocation();
// 立即设置页面标题(使用已从缓存读取的配置) // 立即设置页面标题(使用已从缓存读取的配置)
useEffect(() => { useEffect(() => {
document.title = siteConfig.name; document.title = siteConfig.name;
@@ -105,106 +108,108 @@ function App() {
}, []); }, []);
return ( return (
<Routes> <AnimatePresence mode="wait">
<Route element={<LoginRoute />} path="/" /> <Routes key={location.pathname} location={location}>
<Route <Route element={<LoginRoute />} path="/" />
element={ <Route
<ProtectedRoute skipLayout={true}> element={
<ChangePasswordPage /> <ProtectedRoute skipLayout={true}>
</ProtectedRoute> <ChangePasswordPage />
} </ProtectedRoute>
path="/change-password" }
/> path="/change-password"
<Route />
element={ <Route
<ProtectedRoute> element={
<DashboardPage /> <ProtectedRoute>
</ProtectedRoute> <DashboardPage />
} </ProtectedRoute>
path="/dashboard" }
/> path="/dashboard"
<Route />
element={ <Route
<ProtectedRoute> element={
<MonitorPage /> <ProtectedRoute>
</ProtectedRoute> <MonitorPage />
} </ProtectedRoute>
path="/monitor" }
/> path="/monitor"
<Route />
element={ <Route
<ProtectedRoute> element={
<ForwardPage /> <ProtectedRoute>
</ProtectedRoute> <ForwardPage />
} </ProtectedRoute>
path="/forward" }
/> path="/forward"
<Route />
element={ <Route
<ProtectedRoute> element={
<TunnelPage /> <ProtectedRoute>
</ProtectedRoute> <TunnelPage />
} </ProtectedRoute>
path="/tunnel" }
/> path="/tunnel"
<Route />
element={ <Route
<ProtectedRoute> element={
<NodePage /> <ProtectedRoute>
</ProtectedRoute> <NodePage />
} </ProtectedRoute>
path="/node" }
/> path="/node"
<Route />
element={ <Route
<ProtectedRoute useSimpleLayout={true}> element={
<UserPage /> <ProtectedRoute useSimpleLayout={true}>
</ProtectedRoute> <UserPage />
} </ProtectedRoute>
path="/user" }
/> path="/user"
<Route />
element={ <Route
<ProtectedRoute useSimpleLayout={true}> element={
<GroupPage /> <ProtectedRoute useSimpleLayout={true}>
</ProtectedRoute> <GroupPage />
} </ProtectedRoute>
path="/group" }
/> path="/group"
<Route />
element={ <Route
<ProtectedRoute> element={
<ProfilePage /> <ProtectedRoute>
</ProtectedRoute> <ProfilePage />
} </ProtectedRoute>
path="/profile" }
/> path="/profile"
<Route />
element={ <Route
<ProtectedRoute useSimpleLayout={true}> element={
<LimitPage /> <ProtectedRoute useSimpleLayout={true}>
</ProtectedRoute> <LimitPage />
} </ProtectedRoute>
path="/limit" }
/> path="/limit"
<Route />
element={ <Route
<ProtectedRoute> element={
<ConfigPage /> <ProtectedRoute>
</ProtectedRoute> <ConfigPage />
} </ProtectedRoute>
path="/config" }
/> path="/config"
<Route />
element={ <Route
<ProtectedRoute useSimpleLayout={true}> element={
<PanelSharingPage /> <ProtectedRoute useSimpleLayout={true}>
</ProtectedRoute> <PanelSharingPage />
} </ProtectedRoute>
path="/panel-sharing" }
/> path="/panel-sharing"
<Route element={<SettingsPage />} path="/settings" /> />
</Routes> <Route element={<SettingsPage />} path="/settings" />
</Routes>
</AnimatePresence>
); );
} }
+10 -8
View File
@@ -973,12 +973,13 @@ const SortableCompactTableRow = ({
<TableCell <TableCell
className={`whitespace-nowrap text-foreground ${selectedIds.has(forward.id) ? "bg-primary-50/70 dark:bg-primary-900/40" : ""}`} className={`whitespace-nowrap text-foreground ${selectedIds.has(forward.id) ? "bg-primary-50/70 dark:bg-primary-900/40" : ""}`}
> >
<span <button
className="cursor-pointer hover:text-primary transition-colors" className="cursor-pointer hover:text-primary transition-colors text-left bg-transparent border-none p-0 outline-none focus:ring-2 focus:ring-primary focus:ring-offset-1 rounded-sm"
type="button"
onClick={() => copyToClipboard(forward.name, "规则名")} onClick={() => copyToClipboard(forward.name, "规则名")}
> >
{forward.name} {forward.name}
</span> </button>
</TableCell> </TableCell>
<TableCell <TableCell
className={`whitespace-nowrap ${selectedIds.has(forward.id) ? "bg-primary-50/70 dark:bg-primary-900/40" : ""}`} className={`whitespace-nowrap ${selectedIds.has(forward.id) ? "bg-primary-50/70 dark:bg-primary-900/40" : ""}`}
@@ -987,11 +988,12 @@ const SortableCompactTableRow = ({
<span className="font-medium text-default-700 text-sm"> <span className="font-medium text-default-700 text-sm">
{forward.tunnelName} {forward.tunnelName}
</span> </span>
{forward.tunnelTrafficRatio !== undefined && normalizeTunnelTrafficRatio(forward.tunnelTrafficRatio) !== 1 && ( {forward.tunnelTrafficRatio !== undefined &&
<span className="text-success font-bold text-[12px] ml-1.5 border border-success/30 rounded px-1 bg-success/10"> normalizeTunnelTrafficRatio(forward.tunnelTrafficRatio) !== 1 && (
{formatTunnelTrafficRatio(forward.tunnelTrafficRatio)} <span className="text-success font-bold text-[12px] ml-1.5 border border-success/30 rounded px-1 bg-success/10">
</span> {formatTunnelTrafficRatio(forward.tunnelTrafficRatio)}
)} </span>
)}
</div> </div>
</TableCell> </TableCell>
<TableCell <TableCell
+1 -1
View File
@@ -1,6 +1,6 @@
@import "tailwindcss"; @import "tailwindcss";
@import "tw-animate-css";
@import "./tailwind-theme.pcss"; @import "./tailwind-theme.pcss";
@plugin "tailwindcss-animate";
/* GPU acceleration helper - prevents font blurriness during transforms */ /* GPU acceleration helper - prevents font blurriness during transforms */
.gpu-accelerated { .gpu-accelerated {