[eric] dashboard: toolbar tabs pop in staggered, chat button bounces on an empty canvas

This commit is contained in:
ciregenz
2026-06-04 11:13:46 -07:00
parent 0446e9beb9
commit 6b737f2d8e
2 changed files with 39 additions and 13 deletions
@@ -26,6 +26,14 @@ function ChatBubbleTeardrop(props: { sx?: { fontSize?: number } }) {
</svg>
);
}
// Collapsed-row buttons hop up one after another when the toolbar appears.
const popIn = (i: number) => ({
animation: `toolbar-pop 0.4s cubic-bezier(0.2, 1.4, 0.4, 1) ${i * 55}ms both`,
'@keyframes toolbar-pop': {
from: { opacity: 0, transform: 'translateY(14px)' },
to: { opacity: 1, transform: 'translateY(0)' },
},
});
import GridViewRoundedIcon from '@mui/icons-material/GridViewRounded';
import StickyNote2OutlinedIcon from '@mui/icons-material/StickyNote2Outlined';
import HistoryRoundedIcon from '@mui/icons-material/HistoryRounded';
@@ -641,17 +649,20 @@ const DashboardToolbar = React.forwardRef<HTMLDivElement, Props>(
transition: 'background-color 0.15s',
'&:hover': { bgcolor: c.accent.hover },
'&:active': { bgcolor: c.accent.pressed },
...(newAgentBounce && {
animation: 'new-agent-bounce 1.6s ease-out infinite',
'@keyframes new-agent-bounce': {
'0%': { transform: 'translateY(0)' },
'15%': { transform: 'translateY(-10px)' },
'30%': { transform: 'translateY(0)' },
'42%': { transform: 'translateY(-4px)' },
'55%': { transform: 'translateY(0)' },
'100%': { transform: 'translateY(0)' },
},
}),
// Pop in first; the empty-canvas bounce takes over once the row has settled.
animation: `toolbar-pop 0.4s cubic-bezier(0.2, 1.4, 0.4, 1) both${newAgentBounce ? ', new-agent-bounce 1.6s ease-out 0.6s infinite' : ''}`,
'@keyframes toolbar-pop': {
from: { opacity: 0, transform: 'translateY(14px)' },
to: { opacity: 1, transform: 'translateY(0)' },
},
'@keyframes new-agent-bounce': {
'0%': { transform: 'translateY(0)' },
'15%': { transform: 'translateY(-10px)' },
'30%': { transform: 'translateY(0)' },
'42%': { transform: 'translateY(-4px)' },
'55%': { transform: 'translateY(0)' },
'100%': { transform: 'translateY(0)' },
},
}}
>
<ChatBubbleTeardrop sx={{ fontSize: 18 }} />
@@ -686,6 +697,7 @@ const DashboardToolbar = React.forwardRef<HTMLDivElement, Props>(
cursor: 'pointer',
transition: 'opacity 0.15s, background-color 0.15s',
'&:hover': { opacity: 1, bgcolor: c.bg.secondary, color: c.accent.primary },
...popIn(1),
}}
>
<GridViewRoundedIcon sx={{ fontSize: 22 }} />
@@ -720,6 +732,7 @@ const DashboardToolbar = React.forwardRef<HTMLDivElement, Props>(
cursor: 'pointer',
transition: 'opacity 0.15s, background-color 0.15s',
'&:hover': { opacity: 1, bgcolor: c.bg.secondary, color: c.accent.primary },
...popIn(2),
}}
>
<LanguageIcon sx={{ fontSize: 22 }} />
@@ -753,6 +766,7 @@ const DashboardToolbar = React.forwardRef<HTMLDivElement, Props>(
cursor: 'pointer',
transition: 'opacity 0.15s, background-color 0.15s',
'&:hover': { opacity: 1, bgcolor: c.bg.secondary, color: c.accent.primary },
...popIn(3),
}}
>
<HistoryRoundedIcon sx={{ fontSize: 22 }} />
@@ -787,6 +801,7 @@ const DashboardToolbar = React.forwardRef<HTMLDivElement, Props>(
cursor: 'pointer',
transition: 'opacity 0.15s, background-color 0.15s',
'&:hover': { opacity: 1, bgcolor: c.bg.secondary, color: c.accent.primary },
...popIn(4),
}}
>
<StickyNote2OutlinedIcon sx={{ fontSize: 22 }} />
@@ -1,4 +1,4 @@
import { useCallback, useMemo, useRef } from 'react';
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
import { useElementSelection } from '@/app/components/editor/ElementSelectionContext';
import { useCanvasControls } from '../interaction/useCanvasControls';
@@ -60,6 +60,14 @@ export function useDashboardController(dashboardId: string, isActive: boolean) {
revealSpawnedRef, hasFittedRef, restoredExpandedRef,
} = useDashboardUiState(selection, cards);
// Nudge the chat button while the canvas is empty; the first click dismisses it for this visit.
const bounceDismissedRef = useRef(false);
const canvasEmpty = layoutInitialized && sessionList.length === 0
&& Object.keys(viewCards).length === 0 && Object.keys(browserCards).length === 0;
useEffect(() => {
setNewAgentBounce(canvasEmpty && !bounceDismissedRef.current);
}, [canvasEmpty, setNewAgentBounce]);
const canvasStateRef = useRef({ panX: canvas.panX, panY: canvas.panY, zoom: canvas.zoom });
canvasStateRef.current = { panX: canvas.panX, panY: canvas.panY, zoom: canvas.zoom };
// Stable getter , AgentCards read pan/zoom on demand during drag math.
@@ -265,7 +273,10 @@ export function useDashboardController(dashboardId: string, isActive: boolean) {
onHistoryResume: handleHistoryResume,
onAddBrowser: handleAddBrowser,
onAddNote: handleAddNote,
onNewAgentBounceEnd: () => setNewAgentBounce(false),
onNewAgentBounceEnd: () => {
bounceDismissedRef.current = true;
setNewAgentBounce(false);
},
onFitToView: handleFitToView,
onTidy: handleTidy,
onSearchPaletteClose: () => setSearchPaletteOpen(false),