diff --git a/frontend/src/app/pages/Dashboard/hooks/interaction/useCanvasControls.ts b/frontend/src/app/pages/Dashboard/hooks/interaction/useCanvasControls.ts index 5d661e6a..edacea0b 100644 --- a/frontend/src/app/pages/Dashboard/hooks/interaction/useCanvasControls.ts +++ b/frontend/src/app/pages/Dashboard/hooks/interaction/useCanvasControls.ts @@ -727,10 +727,12 @@ export function useCanvasControls(zoomSensitivity: number = 50, contentBounds?: floor, ceiling, ); - // Content wider/taller than the viewport only happens when the zoom floor bit; centering then hides both ends, so anchor the start of it instead. - const targetPanX = contentWidth * targetZoom > vRect.width - ? padX - minX * targetZoom - : (vRect.width - contentWidth * targetZoom) / 2 - minX * targetZoom; + // Centering is right until the zoom floor bites and the content outgrows its margins: then the left + // edge slides under the dock (or off screen), so never let it start left of the inset. + const targetPanX = Math.max( + (vRect.width - contentWidth * targetZoom) / 2 - minX * targetZoom, + padX - minX * targetZoom, + ); // A single card normally top-biases (header up top, no dead space below). On creation we want the opposite: the new card dead-centered "in front of you", so `centered` forces true vertical centering. const topBiased = cardRects.length === 1 && !centered; const targetPanY = topBiased || contentHeight * targetZoom > vRect.height diff --git a/frontend/src/app/pages/Dashboard/hooks/lifecycle/useDashboardCardActions.ts b/frontend/src/app/pages/Dashboard/hooks/lifecycle/useDashboardCardActions.ts index 750cd57e..a9482142 100644 --- a/frontend/src/app/pages/Dashboard/hooks/lifecycle/useDashboardCardActions.ts +++ b/frontend/src/app/pages/Dashboard/hooks/lifecycle/useDashboardCardActions.ts @@ -17,6 +17,9 @@ import type { CardType, useDashboardSelection } from '../state/useDashboardSelec import type { CanvasActions } from '../interaction/useCanvasControls'; import { useSpawnPlacement } from './useSpawnPlacement'; +// Title bubble + cost line, the strip an expanded card floats above itself. +const EXPANDED_HEADER_H = 64; + type Selection = ReturnType; interface UseDashboardCardActionsArgs { @@ -114,10 +117,14 @@ export function useDashboardCardActions({ workflowsMonitorCard: tidiedMonitor, settingsCard: tidiedSettings, } = store.getState().dashboardLayout; const allRects = [ - ...Object.values(tidied).map((c) => ({ - x: c.x, y: c.y, width: c.width, - height: expandedSet.has(c.session_id) ? Math.max(EXPANDED_CARD_MIN_H, c.height) : c.height, - })), + ...Object.values(tidied).map((c) => { + // An expanded card wears its title bubble ABOVE its rect, so the camera has to be told about that strip or Tidy frames the card and beheads it. + const isExpanded = expandedSet.has(c.session_id); + const height = isExpanded ? Math.max(EXPANDED_CARD_MIN_H, c.height) : c.height; + return isExpanded + ? { x: c.x, y: c.y - EXPANDED_HEADER_H, width: c.width, height: height + EXPANDED_HEADER_H } + : { x: c.x, y: c.y, width: c.width, height }; + }), ...Object.values(tidiedViews).map((c) => ({ x: c.x, y: c.y, width: c.width, height: c.height })), ...Object.values(tidiedBrowsers).map((c) => ({ x: c.x, y: c.y, width: c.width, height: c.height })), ...Object.values(tidiedWorkflows).map((c) => ({ x: c.x, y: c.y, width: c.width, height: c.height })),