From 61df59853b79090b13ff65382d6b1438cb7cf99d Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Thu, 16 Apr 2026 13:39:48 +0200 Subject: [PATCH] feat: add expand/collapse animation for compact card toggle Co-Authored-By: Claude Sonnet 4.6 --- ui/src/pages/AgentHealth/AgentHealth.tsx | 68 ++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 4 deletions(-) diff --git a/ui/src/pages/AgentHealth/AgentHealth.tsx b/ui/src/pages/AgentHealth/AgentHealth.tsx index 0087b644..dcb6048a 100644 --- a/ui/src/pages/AgentHealth/AgentHealth.tsx +++ b/ui/src/pages/AgentHealth/AgentHealth.tsx @@ -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>(new Map()); + const animationTimers = useRef>>(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>({}); @@ -653,7 +704,16 @@ export default function AgentHealth() {
{groups.map((group) => expandedApps.has(group.appId) ? ( -
+