[eric] dashboard: spawn lands in the closest open gap to viewport center; spawn-framing zoom capped 1.15 -> 0.75

This commit is contained in:
ciregenz
2026-07-15 16:14:14 -07:00
parent 3617ed08cb
commit 4e9bd406a7
4 changed files with 21 additions and 10 deletions
@@ -21,6 +21,7 @@ import {
EXPANDED_CARD_MIN_H,
GRID_GAP,
type CardPosition,
SPAWN_FOCUS_MAX_ZOOM,
} from '@/shared/state/dashboardLayoutSlice';
import { generateDashboardName } from '@/shared/state/dashboardsSlice';
import type { ContextPath } from '@/app/components/editor/DirectoryBrowser';
@@ -198,7 +199,7 @@ export function useAgentSpawn({
if (bc) rects.push({ x: bc.x, y: bc.y, width: bc.width, height: bc.height });
}
}
canvasActions.fitToCards(rects, 1.15, true, undefined, true);
canvasActions.fitToCards(rects, SPAWN_FOCUS_MAX_ZOOM, true, undefined, true);
handleHighlightCard(draftId);
}
@@ -16,6 +16,7 @@ import {
DEFAULT_NOTE_W,
DEFAULT_NOTE_H,
EXPANDED_CARD_MIN_H,
SPAWN_FOCUS_MAX_ZOOM,
} from '@/shared/state/dashboardLayoutSlice';
import type { CardType, useDashboardSelection } from '../state/useDashboardSelection';
import type { CanvasActions } from '../interaction/useCanvasControls';
@@ -65,7 +66,7 @@ export function useDashboardCardActions({
}
const card = viewCards[focusKey];
if (card) {
canvasActions.fitToCards([{ x: card.x, y: card.y, width: card.width, height: card.height }], 1.15, true, undefined, true);
canvasActions.fitToCards([{ x: card.x, y: card.y, width: card.width, height: card.height }], SPAWN_FOCUS_MAX_ZOOM, true, undefined, true);
handleHighlightCard(focusKey);
}
}, 200);
@@ -88,7 +89,7 @@ export function useDashboardCardActions({
const newId = Object.keys(allNotes).find((id) => !prevIds.has(id));
if (newId) {
const note = allNotes[newId];
canvasActions.fitToCards([{ x: note.x, y: note.y, width: note.width, height: note.height }], 1.15, true, undefined, true);
canvasActions.fitToCards([{ x: note.x, y: note.y, width: note.width, height: note.height }], SPAWN_FOCUS_MAX_ZOOM, true, undefined, true);
handleHighlightCard(newId);
}
}, 200);
@@ -109,7 +110,7 @@ export function useDashboardCardActions({
setTimeout(() => {
const card = store.getState().dashboardLayout.cards[sessionId];
if (card) {
canvasActions.fitToCards([{ x: card.x, y: card.y, width: card.width, height: card.height }], 1.15, true);
canvasActions.fitToCards([{ x: card.x, y: card.y, width: card.width, height: card.height }], SPAWN_FOCUS_MAX_ZOOM, true);
handleHighlightCard(sessionId);
}
}, 200);
@@ -20,6 +20,7 @@ import {
clearPendingFocusWorkflowId,
clearPendingFocusWorkflowsHub,
type ViewCardPosition,
SPAWN_FOCUS_MAX_ZOOM,
} from '@/shared/state/dashboardLayoutSlice';
import { fetchOutputs, type Output } from '@/shared/state/outputsSlice';
import { generateDashboardName } from '@/shared/state/dashboardsSlice';
@@ -215,7 +216,7 @@ export function useDashboardLifecycle({
setTimeout(() => {
const card = store.getState().dashboardLayout.cards[agentId];
if (card) {
canvasActions.fitToCards([{ x: card.x, y: card.y, width: card.width, height: card.height }], 1.15, true);
canvasActions.fitToCards([{ x: card.x, y: card.y, width: card.width, height: card.height }], SPAWN_FOCUS_MAX_ZOOM, true);
handleHighlightCard(agentId);
}
}, 350);
@@ -253,7 +254,7 @@ export function useDashboardLifecycle({
setTimeout(() => {
const card = store.getState().dashboardLayout.viewCards[cardKey];
if (card) {
canvasActions.fitToCards([{ x: card.x, y: card.y, width: card.width, height: card.height }], 1.15, true);
canvasActions.fitToCards([{ x: card.x, y: card.y, width: card.width, height: card.height }], SPAWN_FOCUS_MAX_ZOOM, true);
handleHighlightCard(cardKey);
}
}, 200);
@@ -270,7 +271,7 @@ export function useDashboardLifecycle({
if (card) {
canvasActions.fitToCards(
[{ x: card.x, y: card.y, width: card.width, height: card.height }],
1.15,
SPAWN_FOCUS_MAX_ZOOM,
true,
);
handleHighlightCard(workflowId);
@@ -352,7 +353,7 @@ export function useDashboardLifecycle({
const rects = [{ x: vc.x, y: vc.y, width: vc.width, height: vc.height }];
const ac = store.getState().dashboardLayout.cards[sid];
if (ac) rects.push({ x: ac.x, y: ac.y, width: ac.width, height: ac.height });
canvasActions.fitToCards(rects, 1.15, true);
canvasActions.fitToCards(rects, SPAWN_FOCUS_MAX_ZOOM, true);
handleHighlightCard(outputId);
}, 200);
}
@@ -26,6 +26,8 @@ export const DEFAULT_WORKFLOWS_HUB_W = DEFAULT_BROWSER_CARD_W;
export const DEFAULT_WORKFLOWS_HUB_H = DEFAULT_BROWSER_CARD_H;
export const EXPANDED_CARD_MIN_H = 620;
export const GRID_GAP = 24;
// Zoom ceiling when the camera frames a freshly spawned card. 1.15 slammed the user to 115% on every spawn; 0.75 keeps the new card readable without the lurch-into-your-face zoom.
export const SPAWN_FOCUS_MAX_ZOOM = 0.75;
// Gap between the Workflows window and the cards it spawns (run monitor, that monitor's browser). Keeps the hub -> monitor -> browser row evenly spaced.
export const WORKFLOW_CARD_GAP = 140;
const GRID_ORIGIN = { x: 40, y: 100 };
@@ -505,8 +507,14 @@ export function computeSpawnPosition(
return placeBesideCard(state, anchor.beside, newW, newH, expandedSessionIds);
}
if (anchor.viewportCenter) {
// Land dead-center, "in front of you", even if a card is already there. Overlap is intentional (new card sits on top via its higher zOrder); dodging to free space is exactly the "spawned off to the side" behavior we're removing.
return { x: anchor.viewportCenter.x - newW / 2, y: anchor.viewportCenter.y - newH / 2 };
// Closest open gap to the viewport center: dead-center-with-overlap stacked spawns invisibly on top of each other (two center spawns in a row = the second fully covers the first). The spiral stays center-biased so it still reads as "in front of you".
return findOpenSpotNear(
anchor.viewportCenter.x - newW / 2,
anchor.viewportCenter.y - newH / 2,
collectOccupiedRects(state, expandedSessionIds),
newW,
newH,
);
}
return findOpenGridCell(collectOccupiedRects(state, expandedSessionIds), newW, newH);
}