feat: add expand/collapse animation for compact card toggle

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-16 13:39:48 +02:00
parent 5229e08b27
commit 61df59853b

View File

@@ -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)}
/>
),
)}