From 6b737f2d8e32630e5930d8da8bda2d67b1215742 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Thu, 4 Jun 2026 11:13:46 -0700 Subject: [PATCH] [eric] dashboard: toolbar tabs pop in staggered, chat button bounces on an empty canvas --- .../app/pages/Dashboard/DashboardToolbar.tsx | 37 +++++++++++++------ .../hooks/state/useDashboardController.ts | 15 +++++++- 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/frontend/src/app/pages/Dashboard/DashboardToolbar.tsx b/frontend/src/app/pages/Dashboard/DashboardToolbar.tsx index 7ca528ed..604ecfad 100644 --- a/frontend/src/app/pages/Dashboard/DashboardToolbar.tsx +++ b/frontend/src/app/pages/Dashboard/DashboardToolbar.tsx @@ -26,6 +26,14 @@ function ChatBubbleTeardrop(props: { sx?: { fontSize?: number } }) { ); } +// 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( 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)' }, + }, }} > @@ -686,6 +697,7 @@ const DashboardToolbar = React.forwardRef( cursor: 'pointer', transition: 'opacity 0.15s, background-color 0.15s', '&:hover': { opacity: 1, bgcolor: c.bg.secondary, color: c.accent.primary }, + ...popIn(1), }} > @@ -720,6 +732,7 @@ const DashboardToolbar = React.forwardRef( cursor: 'pointer', transition: 'opacity 0.15s, background-color 0.15s', '&:hover': { opacity: 1, bgcolor: c.bg.secondary, color: c.accent.primary }, + ...popIn(2), }} > @@ -753,6 +766,7 @@ const DashboardToolbar = React.forwardRef( cursor: 'pointer', transition: 'opacity 0.15s, background-color 0.15s', '&:hover': { opacity: 1, bgcolor: c.bg.secondary, color: c.accent.primary }, + ...popIn(3), }} > @@ -787,6 +801,7 @@ const DashboardToolbar = React.forwardRef( cursor: 'pointer', transition: 'opacity 0.15s, background-color 0.15s', '&:hover': { opacity: 1, bgcolor: c.bg.secondary, color: c.accent.primary }, + ...popIn(4), }} > diff --git a/frontend/src/app/pages/Dashboard/hooks/state/useDashboardController.ts b/frontend/src/app/pages/Dashboard/hooks/state/useDashboardController.ts index 8e59c42b..d4562256 100644 --- a/frontend/src/app/pages/Dashboard/hooks/state/useDashboardController.ts +++ b/frontend/src/app/pages/Dashboard/hooks/state/useDashboardController.ts @@ -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),