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");
|
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;
|
||||||
}) => {
|
}) => {
|
||||||
|
setIsTransitioning(true);
|
||||||
|
setTransitionPhase('fadeOut');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
setIsAuthenticated(true);
|
setIsAuthenticated(true);
|
||||||
setIsAdmin(authData.isAdmin);
|
setIsAdmin(authData.isAdmin);
|
||||||
setUsername(authData.username);
|
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" ||
|
||||||
@@ -139,16 +172,21 @@ function AppContent() {
|
|||||||
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" }}
|
||||||
>
|
>
|
||||||
|
{showTerminalView && (
|
||||||
|
<div className="animate-in fade-in slide-in-from-right-4 duration-300">
|
||||||
<AppView isTopbarOpen={isTopbarOpen} />
|
<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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user