[Haik]: fixed dashboard z index ordering so most recently clicked card is always on top

This commit is contained in:
haikdc
2026-03-20 22:31:21 -07:00
parent 7df846af20
commit c8b2726ec7
6 changed files with 76 additions and 9 deletions
@@ -185,6 +185,8 @@ interface Props {
onMeasuredHeight?: (sessionId: string, height: number) => void;
snapColumn?: { x: number; width: number };
autoFocusInput?: boolean;
cardZOrder?: number;
onBringToFront?: (id: string, type: 'agent' | 'view' | 'browser') => void;
}
const MIN_W = 480;
@@ -201,7 +203,7 @@ const SNAP_THRESHOLD = 60;
const AgentCard: React.FC<Props> = ({
session, expanded, cardX, cardY, cardWidth, cardHeight, zoom = 1, spawnFrom, exitTarget,
isSelected = false, isHighlighted = false, multiDragDelta, onCardSelect, onDragStart, onDragMove, onDragEnd,
onBranch, onMeasuredHeight, snapColumn, autoFocusInput,
onBranch, onMeasuredHeight, snapColumn, autoFocusInput, cardZOrder = 0, onBringToFront,
}) => {
const c = useClaudeTokens();
const dispatch = useAppDispatch();
@@ -464,9 +466,10 @@ const AgentCard: React.FC<Props> = ({
animate={{ opacity: 1, scale: 1, left: activeX, top: activeY }}
exit={exitAnimation}
transition={spawnTransition}
onPointerDownCapture={() => onBringToFront?.(session.id, 'agent')}
style={{
position: 'absolute',
zIndex: isDragging || isResizing ? 999 : expanded ? 100 : 'auto',
zIndex: isDragging || isResizing ? 999999 : cardZOrder,
}}
>
<Box
@@ -539,7 +542,6 @@ const AgentCard: React.FC<Props> = ({
boxShadow: c.shadow.sm,
},
},
zIndex: 50,
}),
...(!isHighlighted && isGlowingRedux && !glowFading && {
animation: 'agent-card-glow-pulse 2s ease-in-out infinite',
@@ -101,12 +101,15 @@ interface Props {
onDragStart?: (id: string, type: 'agent' | 'view' | 'browser') => void;
onDragMove?: (dx: number, dy: number) => void;
onDragEnd?: (dx: number, dy: number, didDrag: boolean) => void;
cardZOrder?: number;
onBringToFront?: (id: string, type: 'agent' | 'view' | 'browser') => void;
}
const BrowserCard: React.FC<Props> = ({
browserId, tabs, activeTabId, cardX, cardY, cardWidth, cardHeight, zoom = 1, cmdHeld = false,
isSelected = false, isHighlighted = false, multiDragDelta, onCardSelect, onDragStart, onDragMove, onDragEnd,
cardZOrder = 0, onBringToFront,
}) => {
const c = useClaudeTokens();
const dispatch = useAppDispatch();
@@ -537,6 +540,7 @@ const BrowserCard: React.FC<Props> = ({
data-select-type="browser-card"
data-select-id={browserId}
data-select-meta={JSON.stringify({ name: activeTitle || 'Browser', url: activeUrl })}
onPointerDownCapture={() => onBringToFront?.(browserId, 'browser')}
onClick={(e: React.MouseEvent) => {
if (justDraggedRef.current) return;
onCardSelect?.(browserId, 'browser', e.shiftKey);
@@ -554,7 +558,7 @@ const BrowserCard: React.FC<Props> = ({
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
zIndex: isHighlighted ? 50 : (isDragging || isResizing) ? 100 : (agentActive || showGlow) ? 50 : 1,
zIndex: (isDragging || isResizing) ? 999999 : cardZOrder,
transition: noTransition ? 'none' : 'box-shadow 0.4s ease, border 0.3s ease',
'&:hover .resize-handle': { opacity: 1 },
...(isHighlighted && {
@@ -35,6 +35,7 @@ import {
pasteBrowserCard,
placeCard,
removeCard,
bringToFront,
setGlowingAgentCard,
clearGlowingAgentCard,
DEFAULT_CARD_W,
@@ -205,6 +206,10 @@ const DashboardInner: React.FC = () => {
selection.selectCard(id, type, shiftKey);
}, [selection]);
const handleBringToFront = useCallback((id: string, type: CardType) => {
dispatch(bringToFront({ id, type }));
}, [dispatch]);
// ---- Viewport event handlers (compose pan + marquee) ----
const handleViewportMouseDown = useCallback((e: React.MouseEvent) => {
if (e.button === 1) {
@@ -1233,6 +1238,7 @@ const DashboardInner: React.FC = () => {
cardY={card.y}
cardWidth={card.width}
cardHeight={card.height}
cardZOrder={card.zOrder ?? 0}
zoom={canvas.zoom}
spawnFrom={origin}
exitTarget={exitTarget}
@@ -1247,6 +1253,7 @@ const DashboardInner: React.FC = () => {
onMeasuredHeight={handleMeasuredHeight}
snapColumn={snapColumn}
autoFocusInput={autoFocusSessionId === session.id}
onBringToFront={handleBringToFront}
/>
);
})}
@@ -1262,6 +1269,7 @@ const DashboardInner: React.FC = () => {
cardY={vc.y}
cardWidth={vc.width}
cardHeight={vc.height}
cardZOrder={vc.zOrder ?? 0}
zoom={canvas.zoom}
cmdHeld={canvas.cmdHeld}
isSelected={selection.isSelected(vc.output_id)}
@@ -1271,6 +1279,7 @@ const DashboardInner: React.FC = () => {
onDragStart={handleCardDragStart}
onDragMove={handleCardDragMove}
onDragEnd={handleCardDragEnd}
onBringToFront={handleBringToFront}
/>
);
})}
@@ -1284,6 +1293,7 @@ const DashboardInner: React.FC = () => {
cardY={bc.y}
cardWidth={bc.width}
cardHeight={bc.height}
cardZOrder={bc.zOrder ?? 0}
zoom={canvas.zoom}
cmdHeld={canvas.cmdHeld}
isSelected={selection.isSelected(bc.browser_id)}
@@ -1293,6 +1303,7 @@ const DashboardInner: React.FC = () => {
onDragStart={handleCardDragStart}
onDragMove={handleCardDragMove}
onDragEnd={handleCardDragEnd}
onBringToFront={handleBringToFront}
/>
))}
{/* Marquee selection rectangle */}
@@ -54,11 +54,14 @@ interface Props {
onDragStart?: (id: string, type: 'agent' | 'view') => void;
onDragMove?: (dx: number, dy: number) => void;
onDragEnd?: (dx: number, dy: number, didDrag: boolean) => void;
cardZOrder?: number;
onBringToFront?: (id: string, type: 'agent' | 'view' | 'browser') => void;
}
const DashboardViewCard: React.FC<Props> = ({
output, cardX, cardY, cardWidth, cardHeight, zoom = 1, cmdHeld = false,
isSelected = false, isHighlighted = false, multiDragDelta, onCardSelect, onDragStart, onDragMove, onDragEnd,
cardZOrder = 0, onBringToFront,
}) => {
const c = useClaudeTokens();
const dispatch = useAppDispatch();
@@ -259,6 +262,7 @@ const DashboardViewCard: React.FC<Props> = ({
data-select-type="view-card"
data-select-id={output.id}
data-select-meta={JSON.stringify({ name: output.name, description: output.description })}
onPointerDownCapture={() => onBringToFront?.(output.id, 'view')}
onClick={(e: React.MouseEvent) => {
if (justDraggedRef.current) return;
onCardSelect?.(output.id, 'view', e.shiftKey);
@@ -284,7 +288,7 @@ const DashboardViewCard: React.FC<Props> = ({
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
zIndex: isHighlighted ? 50 : (isDragging || isResizing) ? 100 : 1,
zIndex: (isDragging || isResizing) ? 999999 : cardZOrder,
transition: noTransition ? 'none' : 'box-shadow 0.2s',
'&:hover .resize-handle': { opacity: 1 },
...(isHighlighted && {
@@ -21,6 +21,7 @@ export interface CardPosition {
y: number;
width: number;
height: number;
zOrder: number;
}
export interface ViewCardPosition {
@@ -29,6 +30,7 @@ export interface ViewCardPosition {
y: number;
width: number;
height: number;
zOrder: number;
}
export interface BrowserTab {
@@ -47,6 +49,7 @@ export interface BrowserCardPosition {
y: number;
width: number;
height: number;
zOrder: number;
}
export interface DashboardLayoutState {
@@ -57,6 +60,7 @@ export interface DashboardLayoutState {
glowingBrowserCards: Record<string, string>;
glowingAgentCards: Record<string, { sourceId: string; fading: boolean; sourceYRatio?: number; label?: string }>;
persistedExpandedSessionIds: string[];
nextZOrder: number;
loading: boolean;
initialized: boolean;
}
@@ -69,6 +73,7 @@ const initialState: DashboardLayoutState = {
glowingBrowserCards: {},
glowingAgentCards: {},
persistedExpandedSessionIds: [],
nextZOrder: 1,
loading: false,
initialized: false,
};
@@ -223,7 +228,25 @@ const dashboardLayoutSlice = createSlice({
action: PayloadAction<{ sessionId: string; x: number; y: number; width: number; height: number }>
) {
const { sessionId, x, y, width, height } = action.payload;
state.cards[sessionId] = { session_id: sessionId, x, y, width, height };
state.cards[sessionId] = { session_id: sessionId, x, y, width, height, zOrder: state.nextZOrder++ };
},
bringToFront(
state,
action: PayloadAction<{ id: string; type: 'agent' | 'view' | 'browser' }>,
) {
const { id, type } = action.payload;
const z = state.nextZOrder++;
if (type === 'agent') {
const card = state.cards[id];
if (card) card.zOrder = z;
} else if (type === 'view') {
const card = state.viewCards[id];
if (card) card.zOrder = z;
} else {
const card = state.browserCards[id];
if (card) card.zOrder = z;
}
},
removeCard(state, action: PayloadAction<string>) {
@@ -250,7 +273,7 @@ const dashboardLayoutSlice = createSlice({
if (hasDraftCard && !id.startsWith('draft-')) continue;
const savedPos = state.closedCardPositions[id];
if (savedPos) {
state.cards[id] = { ...savedPos, session_id: id };
state.cards[id] = { ...savedPos, session_id: id, zOrder: savedPos.zOrder || state.nextZOrder++ };
delete state.closedCardPositions[id];
} else {
const rects = collectOccupiedRects(state, expandedSessionIds);
@@ -261,6 +284,7 @@ const dashboardLayoutSlice = createSlice({
y: pos.y,
width: DEFAULT_CARD_W,
height: DEFAULT_CARD_H,
zOrder: state.nextZOrder++,
};
}
}
@@ -334,6 +358,7 @@ const dashboardLayoutSlice = createSlice({
y: posY,
width: width || DEFAULT_VIEW_CARD_W,
height: height || DEFAULT_VIEW_CARD_H,
zOrder: state.nextZOrder++,
};
},
@@ -376,6 +401,7 @@ const dashboardLayoutSlice = createSlice({
y: pos.y,
width: DEFAULT_BROWSER_CARD_W,
height: DEFAULT_BROWSER_CARD_H,
zOrder: state.nextZOrder++,
};
},
@@ -386,6 +412,7 @@ const dashboardLayoutSlice = createSlice({
...card,
width: card.width || DEFAULT_BROWSER_CARD_W,
height: card.height || DEFAULT_BROWSER_CARD_H,
zOrder: card.zOrder || state.nextZOrder++,
};
},
@@ -449,6 +476,7 @@ const dashboardLayoutSlice = createSlice({
y: posY,
width: width || DEFAULT_BROWSER_CARD_W,
height: height || DEFAULT_BROWSER_CARD_H,
zOrder: state.nextZOrder++,
};
},
@@ -645,6 +673,7 @@ const dashboardLayoutSlice = createSlice({
state.glowingBrowserCards = {};
state.glowingAgentCards = {};
state.persistedExpandedSessionIds = [];
state.nextZOrder = 1;
state.initialized = false;
},
@@ -661,6 +690,22 @@ const dashboardLayoutSlice = createSlice({
state.viewCards = action.payload.viewCards;
state.browserCards = action.payload.browserCards;
state.persistedExpandedSessionIds = action.payload.expandedSessionIds;
// Ensure all cards have a zOrder and compute nextZOrder from persisted data
let maxZ = 0;
for (const c of Object.values(state.cards)) {
if (!c.zOrder) c.zOrder = 0;
if (c.zOrder > maxZ) maxZ = c.zOrder;
}
for (const c of Object.values(state.viewCards)) {
if (!c.zOrder) c.zOrder = 0;
if (c.zOrder > maxZ) maxZ = c.zOrder;
}
for (const c of Object.values(state.browserCards)) {
if (!c.zOrder) c.zOrder = 0;
if (c.zOrder > maxZ) maxZ = c.zOrder;
}
state.nextZOrder = maxZ + 1;
})
.addCase(fetchLayout.rejected, (state) => {
state.loading = false;
@@ -671,7 +716,7 @@ const dashboardLayoutSlice = createSlice({
const card = state.cards[draftId];
if (card) {
delete state.cards[draftId];
state.cards[session.id] = { ...card, session_id: session.id };
state.cards[session.id] = { ...card, session_id: session.id, zOrder: state.nextZOrder++ };
}
});
},
@@ -682,6 +727,7 @@ export const {
placeCard,
setCardSize,
removeCard,
bringToFront,
reconcileSessions,
replaceDraftId,
tidyLayout,
File diff suppressed because one or more lines are too long