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:
@@ -23,6 +23,8 @@ function AppContent() {
|
||||
const saved = localStorage.getItem("topNavbarOpen");
|
||||
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 [isCommandPaletteOpen, setIsCommandPaletteOpen] = useState(false);
|
||||
|
||||
@@ -98,13 +100,44 @@ function AppContent() {
|
||||
username: string | null;
|
||||
userId: string | null;
|
||||
}) => {
|
||||
setIsAuthenticated(true);
|
||||
setIsAdmin(authData.isAdmin);
|
||||
setUsername(authData.username);
|
||||
setIsTransitioning(true);
|
||||
setTransitionPhase('fadeOut');
|
||||
|
||||
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 showTerminalView =
|
||||
currentTabData?.type === "terminal" ||
|
||||
@@ -135,20 +168,25 @@ function AppContent() {
|
||||
|
||||
{isAuthenticated && (
|
||||
<LeftSidebar
|
||||
onSelectView={handleSelectView}
|
||||
disabled={!isAuthenticated || authLoading}
|
||||
isAdmin={isAdmin}
|
||||
username={username}
|
||||
>
|
||||
onSelectView={handleSelectView}
|
||||
disabled={!isAuthenticated || authLoading}
|
||||
isAdmin={isAdmin}
|
||||
username={username}
|
||||
onLogout={handleLogout}
|
||||
>
|
||||
<div
|
||||
className="h-screen w-full visible pointer-events-auto static overflow-hidden"
|
||||
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>
|
||||
|
||||
{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
|
||||
onSelectView={handleSelectView}
|
||||
isAuthenticated={isAuthenticated}
|
||||
@@ -160,7 +198,7 @@ function AppContent() {
|
||||
)}
|
||||
|
||||
{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
|
||||
onSelectView={handleSelectView}
|
||||
isTopbarOpen={isTopbarOpen}
|
||||
@@ -171,13 +209,13 @@ function AppContent() {
|
||||
)}
|
||||
|
||||
{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} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{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} />
|
||||
</div>
|
||||
)}
|
||||
@@ -189,6 +227,15 @@ function AppContent() {
|
||||
/>
|
||||
</LeftSidebar>
|
||||
)}
|
||||
|
||||
{isTransitioning && (
|
||||
<div
|
||||
className={`fixed inset-0 bg-background z-[20000] transition-opacity duration-300 ${
|
||||
transitionPhase === 'fadeOut' ? 'opacity-100' : 'opacity-0'
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Toaster
|
||||
position="bottom-right"
|
||||
richColors={false}
|
||||
|
||||
@@ -65,6 +65,7 @@ interface SidebarProps {
|
||||
isAdmin?: boolean;
|
||||
username?: string | null;
|
||||
children?: React.ReactNode;
|
||||
onLogout?: () => void;
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
@@ -87,6 +88,7 @@ export function LeftSidebar({
|
||||
isAdmin,
|
||||
username,
|
||||
children,
|
||||
onLogout,
|
||||
}: SidebarProps): React.ReactElement {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -486,7 +488,7 @@ export function LeftSidebar({
|
||||
)}
|
||||
<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"
|
||||
onClick={handleLogout}
|
||||
onClick={onLogout || handleLogout}
|
||||
>
|
||||
<span>{t("common.logout")}</span>
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"moduleResolution": "nodenext",
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"esModuleInterop": true,
|
||||
"noEmit": false,
|
||||
"outDir": "./dist/backend",
|
||||
"strict": false,
|
||||
|
||||
Reference in New Issue
Block a user