feat: Add smooth macOS-style page transitions

- Add fullscreen crossfade transition for login/logout (300ms fade-out + 400ms fade-in)
- Add slide-in-from-right animation for all page switches (Dashboard, Terminal, SSH Manager, Admin, Profile)
- Fix TypeScript compilation by adding esModuleInterop to tsconfig.node.json
- Pass handleLogout from DesktopApp to LeftSidebar for consistent transition behavior

All page transitions now use Tailwind animate-in utilities with 300ms duration for smooth, native-feeling UX
This commit is contained in:
ZacharyZcR
2025-11-09 08:11:29 +08:00
parent cd1afc9078
commit fe127e045f
3 changed files with 64 additions and 14 deletions

View File

@@ -23,6 +23,8 @@ function AppContent() {
const saved = localStorage.getItem("topNavbarOpen"); const saved = localStorage.getItem("topNavbarOpen");
return saved !== null ? JSON.parse(saved) : true; return saved !== null ? JSON.parse(saved) : true;
}); });
const [isTransitioning, setIsTransitioning] = useState(false);
const [transitionPhase, setTransitionPhase] = useState<'idle' | 'fadeOut' | 'fadeIn'>('idle');
const { currentTab, tabs } = useTabs(); const { currentTab, tabs } = useTabs();
const [isCommandPaletteOpen, setIsCommandPaletteOpen] = useState(false); const [isCommandPaletteOpen, setIsCommandPaletteOpen] = useState(false);
@@ -98,13 +100,44 @@ function AppContent() {
username: string | null; username: string | null;
userId: string | null; userId: string | null;
}) => { }) => {
setIsAuthenticated(true); setIsTransitioning(true);
setIsAdmin(authData.isAdmin); setTransitionPhase('fadeOut');
setUsername(authData.username);
setTimeout(() => {
setIsAuthenticated(true);
setIsAdmin(authData.isAdmin);
setUsername(authData.username);
setTransitionPhase('fadeIn');
setTimeout(() => {
setIsTransitioning(false);
setTransitionPhase('idle');
}, 400);
}, 300);
}, },
[], [],
); );
const handleLogout = useCallback(async () => {
setIsTransitioning(true);
setTransitionPhase('fadeOut');
setTimeout(async () => {
try {
const { logoutUser, isElectron } = await import("@/ui/main-axios.ts");
await logoutUser();
if (isElectron()) {
localStorage.removeItem("jwt");
}
} catch (error) {
console.error("Logout failed:", error);
}
window.location.reload();
}, 300);
}, []);
const currentTabData = tabs.find((tab) => tab.id === currentTab); const currentTabData = tabs.find((tab) => tab.id === currentTab);
const showTerminalView = const showTerminalView =
currentTabData?.type === "terminal" || currentTabData?.type === "terminal" ||
@@ -135,20 +168,25 @@ function AppContent() {
{isAuthenticated && ( {isAuthenticated && (
<LeftSidebar <LeftSidebar
onSelectView={handleSelectView} onSelectView={handleSelectView}
disabled={!isAuthenticated || authLoading} disabled={!isAuthenticated || authLoading}
isAdmin={isAdmin} isAdmin={isAdmin}
username={username} username={username}
> onLogout={handleLogout}
>
<div <div
className="h-screen w-full visible pointer-events-auto static overflow-hidden" className="h-screen w-full visible pointer-events-auto static overflow-hidden"
style={{ display: showTerminalView ? "block" : "none" }} style={{ display: showTerminalView ? "block" : "none" }}
> >
<AppView isTopbarOpen={isTopbarOpen} /> {showTerminalView && (
<div className="animate-in fade-in slide-in-from-right-4 duration-300">
<AppView isTopbarOpen={isTopbarOpen} />
</div>
)}
</div> </div>
{showHome && ( {showHome && (
<div className="h-screen w-full visible pointer-events-auto static overflow-hidden"> <div className="h-screen w-full visible pointer-events-auto static overflow-hidden animate-in fade-in slide-in-from-right-4 duration-300">
<Dashboard <Dashboard
onSelectView={handleSelectView} onSelectView={handleSelectView}
isAuthenticated={isAuthenticated} isAuthenticated={isAuthenticated}
@@ -160,7 +198,7 @@ function AppContent() {
)} )}
{showSshManager && ( {showSshManager && (
<div className="h-screen w-full visible pointer-events-auto static overflow-hidden"> <div className="h-screen w-full visible pointer-events-auto static overflow-hidden animate-in fade-in slide-in-from-right-4 duration-300">
<HostManager <HostManager
onSelectView={handleSelectView} onSelectView={handleSelectView}
isTopbarOpen={isTopbarOpen} isTopbarOpen={isTopbarOpen}
@@ -171,13 +209,13 @@ function AppContent() {
)} )}
{showAdmin && ( {showAdmin && (
<div className="h-screen w-full visible pointer-events-auto static overflow-hidden"> <div className="h-screen w-full visible pointer-events-auto static overflow-hidden animate-in fade-in slide-in-from-right-4 duration-300">
<AdminSettings isTopbarOpen={isTopbarOpen} /> <AdminSettings isTopbarOpen={isTopbarOpen} />
</div> </div>
)} )}
{showProfile && ( {showProfile && (
<div className="h-screen w-full visible pointer-events-auto static overflow-auto"> <div className="h-screen w-full visible pointer-events-auto static overflow-auto animate-in fade-in slide-in-from-right-4 duration-300">
<UserProfile isTopbarOpen={isTopbarOpen} /> <UserProfile isTopbarOpen={isTopbarOpen} />
</div> </div>
)} )}
@@ -189,6 +227,15 @@ function AppContent() {
/> />
</LeftSidebar> </LeftSidebar>
)} )}
{isTransitioning && (
<div
className={`fixed inset-0 bg-background z-[20000] transition-opacity duration-300 ${
transitionPhase === 'fadeOut' ? 'opacity-100' : 'opacity-0'
}`}
/>
)}
<Toaster <Toaster
position="bottom-right" position="bottom-right"
richColors={false} richColors={false}

View File

@@ -65,6 +65,7 @@ interface SidebarProps {
isAdmin?: boolean; isAdmin?: boolean;
username?: string | null; username?: string | null;
children?: React.ReactNode; children?: React.ReactNode;
onLogout?: () => void;
} }
async function handleLogout() { async function handleLogout() {
@@ -87,6 +88,7 @@ export function LeftSidebar({
isAdmin, isAdmin,
username, username,
children, children,
onLogout,
}: SidebarProps): React.ReactElement { }: SidebarProps): React.ReactElement {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -486,7 +488,7 @@ export function LeftSidebar({
)} )}
<DropdownMenuItem <DropdownMenuItem
className="rounded px-2 py-1.5 hover:bg-white/15 hover:text-accent-foreground focus:bg-white/20 focus:text-accent-foreground cursor-pointer focus:outline-none" className="rounded px-2 py-1.5 hover:bg-white/15 hover:text-accent-foreground focus:bg-white/20 focus:text-accent-foreground cursor-pointer focus:outline-none"
onClick={handleLogout} onClick={onLogout || handleLogout}
> >
<span>{t("common.logout")}</span> <span>{t("common.logout")}</span>
</DropdownMenuItem> </DropdownMenuItem>

View File

@@ -8,6 +8,7 @@
"moduleResolution": "nodenext", "moduleResolution": "nodenext",
"verbatimModuleSyntax": true, "verbatimModuleSyntax": true,
"moduleDetection": "force", "moduleDetection": "force",
"esModuleInterop": true,
"noEmit": false, "noEmit": false,
"outDir": "./dist/backend", "outDir": "./dist/backend",
"strict": false, "strict": false,