feat: add expand/collapse animation for compact card toggle
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState, useMemo, useCallback } from 'react';
|
||||
import { useState, useMemo, useCallback, useRef, useEffect } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router';
|
||||
import { ExternalLink, RefreshCw, Pencil, LayoutGrid, List, ChevronRight, ChevronDown } from 'lucide-react';
|
||||
import {
|
||||
@@ -179,6 +179,57 @@ export default function AgentHealth() {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const [animatingApps, setAnimatingApps] = useState<Map<string, 'expanding' | 'collapsing'>>(new Map());
|
||||
const animationTimers = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map());
|
||||
|
||||
const animateToggle = useCallback((appIdToToggle: string) => {
|
||||
// Clear any existing timer for this app
|
||||
const existing = animationTimers.current.get(appIdToToggle);
|
||||
if (existing) clearTimeout(existing);
|
||||
|
||||
const isCurrentlyExpanded = expandedApps.has(appIdToToggle);
|
||||
|
||||
if (isCurrentlyExpanded) {
|
||||
// Collapsing: start animation, then remove from expandedApps after transition
|
||||
setAnimatingApps((prev) => new Map(prev).set(appIdToToggle, 'collapsing'));
|
||||
const timer = setTimeout(() => {
|
||||
setExpandedApps((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(appIdToToggle);
|
||||
return next;
|
||||
});
|
||||
setAnimatingApps((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.delete(appIdToToggle);
|
||||
return next;
|
||||
});
|
||||
animationTimers.current.delete(appIdToToggle);
|
||||
}, 200);
|
||||
animationTimers.current.set(appIdToToggle, timer);
|
||||
} else {
|
||||
// Expanding: add to expandedApps immediately, animate in
|
||||
setExpandedApps((prev) => new Set(prev).add(appIdToToggle));
|
||||
setAnimatingApps((prev) => new Map(prev).set(appIdToToggle, 'expanding'));
|
||||
// Use requestAnimationFrame to ensure the collapsed state renders first
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
setAnimatingApps((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.delete(appIdToToggle);
|
||||
return next;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}, [expandedApps]);
|
||||
|
||||
// Cleanup timers on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
animationTimers.current.forEach((timer) => clearTimeout(timer));
|
||||
};
|
||||
}, []);
|
||||
|
||||
const [configEditing, setConfigEditing] = useState(false);
|
||||
const [configDraft, setConfigDraft] = useState<Record<string, string | boolean>>({});
|
||||
|
||||
@@ -653,7 +704,16 @@ export default function AgentHealth() {
|
||||
<div className={styles.compactGrid}>
|
||||
{groups.map((group) =>
|
||||
expandedApps.has(group.appId) ? (
|
||||
<div key={group.appId} className={styles.compactGridExpanded}>
|
||||
<div
|
||||
key={group.appId}
|
||||
className={`${styles.compactGridExpanded} ${styles.expandWrapper} ${
|
||||
animatingApps.get(group.appId) === 'expanding'
|
||||
? styles.expandWrapperCollapsed
|
||||
: animatingApps.get(group.appId) === 'collapsing'
|
||||
? styles.expandWrapperCollapsed
|
||||
: styles.expandWrapperExpanded
|
||||
}`}
|
||||
>
|
||||
<GroupCard
|
||||
title={group.appId}
|
||||
accent={appHealth(group)}
|
||||
@@ -666,7 +726,7 @@ export default function AgentHealth() {
|
||||
/>
|
||||
<button
|
||||
className={styles.collapseBtn}
|
||||
onClick={() => toggleAppExpanded(group.appId)}
|
||||
onClick={() => animateToggle(group.appId)}
|
||||
title="Collapse"
|
||||
>
|
||||
<ChevronDown size={14} />
|
||||
@@ -717,7 +777,7 @@ export default function AgentHealth() {
|
||||
<CompactAppCard
|
||||
key={group.appId}
|
||||
group={group}
|
||||
onExpand={() => toggleAppExpanded(group.appId)}
|
||||
onExpand={() => animateToggle(group.appId)}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user