feat(ui): add exit animation to FullScreenPanel dialogs

- Wrap FullScreenPanel content with AnimatePresence for exit animation
- Add exit={{ opacity: 0 }} to enable fade-out on close
- Use useRef + useEffect to preserve provider data during exit animation
- Follow React best practices by updating refs in useEffect instead of render

Affected components:
- EditProviderDialog
- UsageScriptModal
- AddProviderDialog
- McpFormModal
- ProxySettingsDialog
This commit is contained in:
Jason
2025-12-22 22:27:32 +08:00
Unverified
parent db8180aa31
commit 7d495aa772
2 changed files with 64 additions and 43 deletions
+22 -4
View File
@@ -65,6 +65,22 @@ function App() {
const [envConflicts, setEnvConflicts] = useState<EnvConflict[]>([]); const [envConflicts, setEnvConflicts] = useState<EnvConflict[]>([]);
const [showEnvBanner, setShowEnvBanner] = useState(false); const [showEnvBanner, setShowEnvBanner] = useState(false);
// 保存最后一个有效的 provider,用于动画退出期间显示内容
const lastUsageProviderRef = useRef<Provider | null>(null);
const lastEditingProviderRef = useRef<Provider | null>(null);
useEffect(() => {
if (usageProvider) {
lastUsageProviderRef.current = usageProvider;
}
}, [usageProvider]);
useEffect(() => {
if (editingProvider) {
lastEditingProviderRef.current = editingProvider;
}
}, [editingProvider]);
const promptPanelRef = useRef<any>(null); const promptPanelRef = useRef<any>(null);
const mcpPanelRef = useRef<any>(null); const mcpPanelRef = useRef<any>(null);
const skillsPageRef = useRef<any>(null); const skillsPageRef = useRef<any>(null);
@@ -639,7 +655,7 @@ function App() {
<EditProviderDialog <EditProviderDialog
open={Boolean(editingProvider)} open={Boolean(editingProvider)}
provider={editingProvider} provider={lastEditingProviderRef.current}
onOpenChange={(open) => { onOpenChange={(open) => {
if (!open) { if (!open) {
setEditingProvider(null); setEditingProvider(null);
@@ -650,14 +666,16 @@ function App() {
isProxyTakeover={isProxyRunning && isCurrentAppTakeoverActive} isProxyTakeover={isProxyRunning && isCurrentAppTakeoverActive}
/> />
{usageProvider && ( {lastUsageProviderRef.current && (
<UsageScriptModal <UsageScriptModal
provider={usageProvider} provider={lastUsageProviderRef.current}
appId={activeApp} appId={activeApp}
isOpen={Boolean(usageProvider)} isOpen={Boolean(usageProvider)}
onClose={() => setUsageProvider(null)} onClose={() => setUsageProvider(null)}
onSave={(script) => { onSave={(script) => {
void saveUsageScript(usageProvider, script); if (usageProvider) {
void saveUsageScript(usageProvider, script);
}
}} }}
/> />
)} )}
+42 -39
View File
@@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import { motion } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import { ArrowLeft } from "lucide-react"; import { ArrowLeft } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -33,49 +33,52 @@ export const FullScreenPanel: React.FC<FullScreenPanelProps> = ({
}; };
}, [isOpen]); }, [isOpen]);
if (!isOpen) return null;
return createPortal( return createPortal(
<motion.div <AnimatePresence>
initial={{ opacity: 0 }} {isOpen && (
animate={{ opacity: 1 }} <motion.div
transition={{ duration: 0.2 }} initial={{ opacity: 0 }}
className="fixed inset-0 z-[60] flex flex-col" animate={{ opacity: 1 }}
style={{ backgroundColor: "hsl(var(--background))" }} exit={{ opacity: 0 }}
> transition={{ duration: 0.2 }}
{/* Header */} className="fixed inset-0 z-[60] flex flex-col"
<div
className="flex-shrink-0 py-3 border-b border-border-default"
style={{ backgroundColor: "hsl(var(--background))" }}
>
<div className="h-4 w-full" data-tauri-drag-region />
<div className="mx-auto max-w-[56rem] px-6 flex items-center gap-4">
<Button type="button" variant="outline" size="icon" onClick={onClose}>
<ArrowLeft className="h-4 w-4" />
</Button>
<h2 className="text-lg font-semibold text-foreground">{title}</h2>
</div>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto scroll-overlay">
<div className="mx-auto max-w-[56rem] px-6 py-6 space-y-6 w-full">
{children}
</div>
</div>
{/* Footer */}
{footer && (
<div
className="flex-shrink-0 py-4 border-t border-border-default"
style={{ backgroundColor: "hsl(var(--background))" }} style={{ backgroundColor: "hsl(var(--background))" }}
> >
<div className="mx-auto max-w-[56rem] px-6 flex items-center justify-end gap-3"> {/* Header */}
{footer} <div
className="flex-shrink-0 py-3 border-b border-border-default"
style={{ backgroundColor: "hsl(var(--background))" }}
>
<div className="h-4 w-full" data-tauri-drag-region />
<div className="mx-auto max-w-[56rem] px-6 flex items-center gap-4">
<Button type="button" variant="outline" size="icon" onClick={onClose}>
<ArrowLeft className="h-4 w-4" />
</Button>
<h2 className="text-lg font-semibold text-foreground">{title}</h2>
</div>
</div> </div>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto scroll-overlay">
<div className="mx-auto max-w-[56rem] px-6 py-6 space-y-6 w-full">
{children}
</div>
</div>
{/* Footer */}
{footer && (
<div
className="flex-shrink-0 py-4 border-t border-border-default"
style={{ backgroundColor: "hsl(var(--background))" }}
>
<div className="mx-auto max-w-[56rem] px-6 flex items-center justify-end gap-3">
{footer}
</div>
</div>
)}
</motion.div>
)} )}
</motion.div>, </AnimatePresence>,
document.body, document.body,
); );
}; };