[eric] canvas: multi-drag followers ride an imperative translate channel, zero renders per drag frame

This commit is contained in:
ciregenz
2026-08-06 09:09:04 -07:00
parent 358017096c
commit 714389381e
14 changed files with 127 additions and 59 deletions
@@ -4,6 +4,7 @@ import { useAppDispatch, useAppSelector } from '@/shared/hooks';
import { addViewCard, clearTiledCard, toggleMinimizeCard, selectFullscreenCardId } from '@/shared/state/dashboardLayoutSlice';
import DashboardHeader from './DashboardHeader';
import TetherLayerHost from './TetherLayerHost';
import { useLiveMultiDrag } from '../hooks/interaction/useLiveMultiDrag';
import DashboardCardLayer from './DashboardCardLayer';
import DashboardOverlays from './DashboardOverlays';
import { useCanvasContextMenu } from './useCanvasContextMenu';
@@ -62,7 +63,7 @@ interface DashboardCanvasProps {
highlightedCardId: string | null;
autoFocusSessionId: string | null;
focusedCardId: string | null;
multiDragDelta: { dx: number; dy: number } | null;
multiDragActive: boolean;
shakeDirection: Direction | null;
neighborDirections: NeighborDirections;
toolbarOpen: boolean;
@@ -124,7 +125,7 @@ const DashboardCanvas: React.FC<DashboardCanvasProps> = ({
highlightedCardId,
autoFocusSessionId,
focusedCardId,
multiDragDelta,
multiDragActive,
shakeDirection,
neighborDirections,
toolbarOpen,
@@ -163,6 +164,7 @@ const DashboardCanvas: React.FC<DashboardCanvasProps> = ({
onTidy,
onSearchPaletteClose,
}) => {
useLiveMultiDrag();
const { accent, gradient } = useThemeAccent();
const { washOpacity, grain } = useThemeWash();
// A single picked color stores gradient=null, so fall back to the accent (mirrors BeatShell).
@@ -469,7 +471,7 @@ const DashboardCanvas: React.FC<DashboardCanvasProps> = ({
highlightedCardId={highlightedCardId}
autoFocusSessionId={autoFocusSessionId}
focusedCardId={focusedCardId}
multiDragDelta={multiDragDelta}
multiDragActive={multiDragActive}
shakeDirection={shakeDirection}
spawnOriginsRef={spawnOriginsRef}
revealSpawnedRef={revealSpawnedRef}
@@ -39,7 +39,7 @@ interface DashboardCardLayerProps {
highlightedCardId: string | null;
autoFocusSessionId: string | null;
focusedCardId: string | null;
multiDragDelta: { dx: number; dy: number } | null;
multiDragActive: boolean;
shakeDirection: Direction | null;
spawnOriginsRef: RefObject<Record<string, SpawnOrigin>>;
revealSpawnedRef: RefObject<Set<string>>;
@@ -71,7 +71,7 @@ const DashboardCardLayer: React.FC<DashboardCardLayerProps> = ({
highlightedCardId,
autoFocusSessionId,
focusedCardId,
multiDragDelta,
multiDragActive,
shakeDirection,
spawnOriginsRef,
revealSpawnedRef,
@@ -153,7 +153,7 @@ const DashboardCardLayer: React.FC<DashboardCardLayerProps> = ({
isSelected={isSel}
isHighlighted={highlightedCardId === sid}
// Only selected cards need the live drag delta; passing it to everyone broke memo equality for unselected cards on every mouse-move during multi-drag.
multiDragDelta={isSel ? multiDragDelta : null}
multiDragActive={isSel && multiDragActive}
onCardSelect={onCardSelect}
onDragStart={onDragStart}
onDragMove={onDragMove}
@@ -187,7 +187,7 @@ const DashboardCardLayer: React.FC<DashboardCardLayerProps> = ({
cmdHeld={cmdHeld}
isSelected={selection.isSelected(cardKey)}
isHighlighted={highlightedCardId === cardKey}
multiDragDelta={multiDragDelta}
multiDragActive={multiDragActive}
onCardSelect={onCardSelect}
onDragStart={onDragStart}
onDragMove={onDragMove}
@@ -214,7 +214,7 @@ const DashboardCardLayer: React.FC<DashboardCardLayerProps> = ({
cmdHeld={cmdHeld}
isSelected={selection.isSelected(bc.browser_id)}
isHighlighted={highlightedCardId === bc.browser_id}
multiDragDelta={multiDragDelta}
multiDragActive={selection.isSelected(bc.browser_id) && multiDragActive}
onCardSelect={onCardSelect}
onDragStart={onDragStart}
onDragMove={onDragMove}
@@ -227,7 +227,7 @@ const DashboardCardLayer: React.FC<DashboardCardLayerProps> = ({
workflowsHub={workflowsHub}
selection={selection}
highlightedCardId={highlightedCardId}
multiDragDelta={multiDragDelta}
multiDragActive={multiDragActive}
getCanvasState={getCanvasState}
onCardSelect={onCardSelect}
onDragStart={onDragStart}
@@ -16,7 +16,7 @@ interface DashboardWindowCardsProps {
workflowsHub: WorkflowsHubPosition | null;
selection: ReturnType<typeof useDashboardSelection>;
highlightedCardId: string | null;
multiDragDelta: { dx: number; dy: number } | null;
multiDragActive: boolean;
getCanvasState: () => { panX: number; panY: number; zoom: number };
onCardSelect: (id: string, type: CardType, shiftKey: boolean, originTarget?: EventTarget | null) => void;
onDragStart: (id: string, type: CardType) => void;
@@ -30,7 +30,7 @@ const DashboardWindowCards: React.FC<DashboardWindowCardsProps> = ({
workflowsHub,
selection,
highlightedCardId,
multiDragDelta,
multiDragActive,
getCanvasState,
onCardSelect,
onDragStart,
@@ -61,7 +61,7 @@ const DashboardWindowCards: React.FC<DashboardWindowCardsProps> = ({
getCanvasState={getCanvasState}
isSelected={selection.isSelected('workflows-hub')}
isHighlighted={highlightedCardId === 'workflows-hub'}
multiDragDelta={selection.isSelected('workflows-hub') ? multiDragDelta : null}
multiDragActive={selection.isSelected('workflows-hub') && multiDragActive}
onCardSelect={onCardSelect}
onDragStart={onDragStart}
onDragMove={onDragMove}
@@ -79,7 +79,7 @@ const DashboardWindowCards: React.FC<DashboardWindowCardsProps> = ({
getCanvasState={getCanvasState}
isSelected={selection.isSelected(SETTINGS_CARD_ID)}
isHighlighted={highlightedCardId === SETTINGS_CARD_ID}
multiDragDelta={selection.isSelected(SETTINGS_CARD_ID) ? multiDragDelta : null}
multiDragActive={selection.isSelected(SETTINGS_CARD_ID) && multiDragActive}
onCardSelect={onCardSelect}
onDragStart={onDragStart}
onDragMove={onDragMove}
@@ -97,7 +97,7 @@ const DashboardWindowCards: React.FC<DashboardWindowCardsProps> = ({
getCanvasState={getCanvasState}
isSelected={selection.isSelected(MARKETPLACE_CARD_ID)}
isHighlighted={highlightedCardId === MARKETPLACE_CARD_ID}
multiDragDelta={selection.isSelected(MARKETPLACE_CARD_ID) ? multiDragDelta : null}
multiDragActive={selection.isSelected(MARKETPLACE_CARD_ID) && multiDragActive}
onCardSelect={onCardSelect}
onDragStart={onDragStart}
onDragMove={onDragMove}
@@ -268,7 +268,7 @@ interface OuterProps {
exitTarget?: { x: number; y: number };
isSelected?: boolean;
isHighlighted?: boolean;
multiDragDelta?: { dx: number; dy: number } | null;
multiDragActive?: boolean;
onCardSelect?: (id: string, type: 'agent' | 'view', shiftKey: boolean) => void;
onDragStart?: (id: string, type: 'agent' | 'view') => void;
onDragMove?: (dx: number, dy: number, mouseX?: number, mouseY?: number) => void;
@@ -305,7 +305,7 @@ const SNAP_THRESHOLD = 60;
const AgentCard: React.FC<Props> = ({
session, expanded: expandedInStore, cardX, cardY, cardWidth, cardHeight, getCanvasState, spawnFrom, exitTarget,
isSelected = false, isHighlighted = false, multiDragDelta, onCardSelect, onDragStart, onDragMove, onDragEnd,
isSelected = false, isHighlighted = false, multiDragActive = false, onCardSelect, onDragStart, onDragMove, onDragEnd,
onBranch, onMeasuredHeight, snapColumn, autoFocusInput, cardZOrder = 0, onDoubleClick, onBringToFront,
shakeDirection,
}) => {
@@ -765,12 +765,10 @@ const AgentCard: React.FC<Props> = ({
const browserShot = useBrowserPillShot(session.id, pillMode && !pillArtifact);
// justDraggedRef: the motion.div parks at the START position for the whole imperative drag, so the end-of-drag commit must snap (not spring) to the final spot or the card visibly re-glides from where the drag began.
const noTransition = isDragging || isResizing || (isSelected && !!multiDragDelta) || justDraggedRef.current;
const noTransition = isDragging || isResizing || (isSelected && multiDragActive) || justDraggedRef.current;
const mdDx = (!isDragging && isSelected && multiDragDelta) ? multiDragDelta.dx : 0;
const mdDy = (!isDragging && isSelected && multiDragDelta) ? multiDragDelta.dy : 0;
const activeX = localResize?.x ?? localDragPos?.x ?? (cardX + mdDx);
const activeY = localResize?.y ?? localDragPos?.y ?? (cardY + mdDy);
const activeX = localResize?.x ?? localDragPos?.x ?? cardX;
const activeY = localResize?.y ?? localDragPos?.y ?? cardY;
const activeW = localResize?.w ?? cardWidth;
const activeH = localResize?.h ?? cardHeight;
const tiledSize = useTiledCard({ cardId: session.id, zone: tileZone, active: true, originX: activeX, originY: activeY, getCamera: getCanvasState });
@@ -189,7 +189,7 @@ interface Props {
cmdHeld?: boolean;
isSelected?: boolean;
isHighlighted?: boolean;
multiDragDelta?: { dx: number; dy: number } | null;
multiDragActive?: boolean;
// Belongs to a non-active dashboard but kept mounted-hidden so its webContents + sessionStorage survive the switch.
keepAliveHidden?: boolean;
onCardSelect?: (id: string, type: 'agent' | 'view' | 'browser', shiftKey: boolean, originTarget?: EventTarget | null) => void;
@@ -204,7 +204,7 @@ interface Props {
const BrowserCard: React.FC<Props> = ({
browserId, tabs, activeTabId, cardX, cardY, cardWidth, cardHeight, getCanvasState, cmdHeld = false,
isSelected = false, isHighlighted = false, keepAliveHidden = false, multiDragDelta, onCardSelect, onDragStart, onDragMove, onDragEnd,
isSelected = false, isHighlighted = false, keepAliveHidden = false, multiDragActive = false, onCardSelect, onDragStart, onDragMove, onDragEnd,
cardZOrder = 0, onDoubleClick, onBringToFront,
}) => {
const c = useClaudeTokens();
@@ -345,11 +345,19 @@ const BrowserCard: React.FC<Props> = ({
const hasDockRect = !!dockRect;
useEffect(() => {
if (!dockedTo || !hasDockRect) return undefined;
// Clear only after having followed: the old unconditional else cleared translate on EVERY
// drag frame of every other card, and would fight the multi-drag channel's writes.
let wasFollowing = false;
const off = subscribeLiveDrag((info) => {
const el = rootElRef.current;
if (!el) return;
if (info && info.cardId === dockedTo) el.style.translate = `${info.dx}px ${info.dy}px`;
else el.style.translate = '';
if (info && info.cardId === dockedTo) {
el.style.translate = `${info.dx}px ${info.dy}px`;
wasFollowing = true;
} else if (wasFollowing) {
el.style.translate = '';
wasFollowing = false;
}
});
return () => {
off();
@@ -1045,13 +1053,11 @@ const BrowserCard: React.FC<Props> = ({
(e.target as HTMLElement).releasePointerCapture(e.pointerId);
}, [computeResize, dispatch, browserId]);
const mdDx = (!isDragging && isSelected && multiDragDelta) ? multiDragDelta.dx : 0;
const mdDy = (!isDragging && isSelected && multiDragDelta) ? multiDragDelta.dy : 0;
const displayX = localResize?.x ?? localDragPos?.x ?? (cardX + mdDx);
const displayY = localResize?.y ?? localDragPos?.y ?? (cardY + mdDy);
const displayX = localResize?.x ?? localDragPos?.x ?? cardX;
const displayY = localResize?.y ?? localDragPos?.y ?? cardY;
const displayW = localResize?.w ?? cardWidth;
const displayH = localResize?.h ?? cardHeight;
const noTransition = isDragging || isResizing || (isSelected && !!multiDragDelta);
const noTransition = isDragging || isResizing || (isSelected && multiDragActive);
// During a drag, move the card by a COMPOSITOR transform, not left/top layout: while edge-panning, the canvas transform and the card's left/top update land a frame apart, and the webview's guest surface follows the transform immediately while left/top relayouts late, so the browser visibly shimmers back and forth. A transform for the drag delta rides the same compositor path as the canvas pan, so they move together in one frame.
const dragging = isDragging && !!localDragPos && !localResize;
const dragTx = dragging ? displayX - cardX : 0;
@@ -42,7 +42,7 @@ interface CanvasWindowCardProps {
background: string; highlightColor: string;
getCanvasState: () => { panX: number; panY: number; zoom: number };
isSelected?: boolean; isHighlighted?: boolean;
multiDragDelta?: { dx: number; dy: number } | null;
multiDragActive?: boolean;
onCardSelect?: (id: string, type: CardType, shiftKey: boolean, originTarget?: EventTarget | null) => void;
onDragStart?: (id: string, type: CardType) => void;
onDragMove?: (dx: number, dy: number, mouseX?: number, mouseY?: number) => void;
@@ -64,7 +64,7 @@ const CanvasWindowCard: React.FC<CanvasWindowCardProps> = ({
cardX, cardY, cardWidth, cardHeight, cardZOrder = 0,
minimized = false, minWidth, minHeight, background, highlightColor,
getCanvasState,
isSelected = false, isHighlighted = false, multiDragDelta = null,
isSelected = false, isHighlighted = false, multiDragActive = false,
onCardSelect, onDragStart, onDragMove, onDragEnd, onBringToFront,
onCommitPosition, onCommitSize,
onMinimize, onClose,
@@ -158,16 +158,14 @@ const CanvasWindowCard: React.FC<CanvasWindowCardProps> = ({
getCanvasState, onCommitPosition, onCommitSize, untileForResize: tiling.untileForResize,
});
const mdDx = (!isDragging && !isResizing && isSelected && multiDragDelta) ? multiDragDelta.dx : 0;
const mdDy = (!isDragging && !isResizing && isSelected && multiDragDelta) ? multiDragDelta.dy : 0;
const dx = (localResize?.x ?? localDragPos?.x ?? cardX) + mdDx;
const dy = (localResize?.y ?? localDragPos?.y ?? cardY) + mdDy;
const dx = localResize?.x ?? localDragPos?.x ?? cardX;
const dy = localResize?.y ?? localDragPos?.y ?? cardY;
const dw = localResize?.w ?? cardWidth;
const dh = localResize?.h ?? cardHeight;
const tiledSize = useTiledCard({ cardId, zone: tiling.zone, active: !minimized, originX: dx, originY: dy, getCamera: getCanvasState });
const border = isHighlighted ? `2px solid ${highlightColor}` : isSelected ? '2px solid #3b82f6' : `1px solid ${c.border.subtle}`;
const noTransition = isDragging || isResizing || (isSelected && !!multiDragDelta);
const noTransition = isDragging || isResizing || (isSelected && multiDragActive);
return (
<div
@@ -95,7 +95,7 @@ interface Props {
cmdHeld?: boolean;
isSelected?: boolean;
isHighlighted?: boolean;
multiDragDelta?: { dx: number; dy: number } | null;
multiDragActive?: boolean;
onCardSelect?: (id: string, type: 'agent' | 'view', shiftKey: boolean) => void;
onDragStart?: (id: string, type: 'agent' | 'view') => void;
onDragMove?: (dx: number, dy: number, mouseX?: number, mouseY?: number) => void;
@@ -142,7 +142,7 @@ const BootingBody: React.FC = () => {
const DashboardViewCard: React.FC<Props> = ({
output, cardKey: cardKeyProp, instance = 1, cardX, cardY, cardWidth, cardHeight, getCanvasState, cmdHeld = false,
isSelected = false, isHighlighted = false, multiDragDelta, onCardSelect, onDragStart, onDragMove, onDragEnd,
isSelected = false, isHighlighted = false, multiDragActive = false, onCardSelect, onDragStart, onDragMove, onDragEnd,
cardZOrder = 0, onDoubleClick, onBringToFront,
}) => {
const cardKey = cardKeyProp ?? output.id;
@@ -613,13 +613,11 @@ const DashboardViewCard: React.FC<Props> = ({
previewRef.current?.reload();
};
const mdDx = (!isDragging && isSelected && multiDragDelta) ? multiDragDelta.dx : 0;
const mdDy = (!isDragging && isSelected && multiDragDelta) ? multiDragDelta.dy : 0;
const displayX = localResize?.x ?? localDragPos?.x ?? (cardX + mdDx);
const displayY = localResize?.y ?? localDragPos?.y ?? (cardY + mdDy);
const displayX = localResize?.x ?? localDragPos?.x ?? cardX;
const displayY = localResize?.y ?? localDragPos?.y ?? cardY;
const displayW = localResize?.w ?? cardWidth;
const displayH = localResize?.h ?? cardHeight;
const noTransition = isDragging || isResizing || (isSelected && !!multiDragDelta);
const noTransition = isDragging || isResizing || (isSelected && multiDragActive);
// Drag via a compositor transform, not left/top: an app card's webview surface shimmers back and forth while edge-panning otherwise (the transform and the late left/top relayout desync a frame). Same fix as BrowserCard.
const dragging = isDragging && !!localDragPos && !localResize;
const dragTx = dragging ? displayX - cardX : 0;
@@ -0,0 +1,21 @@
// Co-selected cards follow a drag OUTSIDE React, same shape as marqueeLiveChannel: per-frame
// deltas at pointer rate re-rendered every selected card's whole subtree (the last ENG-88 hole).
export interface MultiDragUpdate {
ids: ReadonlyArray<string>;
dx: number;
dy: number;
}
type Listener = (update: MultiDragUpdate | null) => void;
const listeners = new Set<Listener>();
export function publishMultiDrag(update: MultiDragUpdate | null): void {
for (const listener of listeners) listener(update);
}
export function subscribeMultiDrag(listener: Listener): () => void {
listeners.add(listener);
return () => { listeners.delete(listener); };
}
@@ -5,6 +5,7 @@ import { moveCards } from '@/shared/state/dashboardLayoutSlice';
import type { CardType, useDashboardSelection } from '../state/useDashboardSelection';
import type { CanvasActions } from './useCanvasControls';
import { publishLiveDrag } from './liveDragChannel';
import { publishMultiDrag } from './multiDragLiveChannel';
import { setCanvasInteractionActive } from '@/shared/canvasInteractionState';
type Selection = ReturnType<typeof useDashboardSelection>;
@@ -34,7 +35,11 @@ export function useCardDrag({
}: UseCardDragArgs) {
const dispatch = useAppDispatch();
const [multiDragDelta, setMultiDragDelta] = useState<{ dx: number; dy: number } | null>(null);
// Per-frame deltas ride multiDragLiveChannel (style writes, zero renders); this flag renders
// exactly twice per gesture and exists so follower cards suppress their spring on the commit.
const [multiDragActive, setMultiDragActive] = useState(false);
const multiDragActiveRef = useRef(false);
const followIdsRef = useRef<string[]>([]);
const activeDragCardRef = useRef<string | null>(null);
const isMultiDragRef = useRef(false);
@@ -86,6 +91,7 @@ export function useCardDrag({
// Multi only when there is actually company: a lone selected card on this path made every drag after the first pay a setState per frame.
if (selection.isSelected(id) && selection.selectedArray().length > 1) {
isMultiDragRef.current = true;
followIdsRef.current = selection.selectedArray().filter((s) => s.id !== id).map((s) => s.id);
} else {
// Grabbing an unselected card SELECTS just it (was deselectAll, which left nothing selected, so the next spawn had no anchor and flew to viewport-center far from the card you just moved). Also survives the stale-read where the capture-phase click already selected it.
selection.selectCard(id, type, false);
@@ -106,7 +112,11 @@ export function useCardDrag({
edgePanFrameRef.current = requestAnimationFrame(tickEdgePan);
}
if (isMultiDragRef.current) {
setMultiDragDelta({ dx, dy });
if (!multiDragActiveRef.current) {
multiDragActiveRef.current = true;
setMultiDragActive(true);
}
publishMultiDrag({ ids: followIdsRef.current, dx, dy });
}
if (activeDragCardRef.current) {
publishLiveDrag({ cardId: activeDragCardRef.current, dx, dy });
@@ -121,8 +131,16 @@ export function useCardDrag({
document.body.classList.remove('dashboard-marquee-active');
setCanvasInteractionActive(false);
isMultiDragRef.current = false;
setMultiDragDelta(null);
followIdsRef.current = [];
publishMultiDrag(null);
publishLiveDrag(null);
if (multiDragActiveRef.current) {
// Stay active through the moveCards commit paint (followers must snap, not spring), then release next frame.
requestAnimationFrame(() => {
multiDragActiveRef.current = false;
setMultiDragActive(false);
});
}
}, [stopEdgePan, canvasActions]);
const handleCardDragEnd = useCallback((dx: number, dy: number, didDrag: boolean) => {
@@ -152,7 +170,7 @@ export function useCardDrag({
}, [clearDrag, stopEdgePan]);
return {
multiDragDelta,
multiDragActive,
handleCardDragStart,
handleCardDragMove,
handleCardDragEnd,
@@ -0,0 +1,27 @@
import { useEffect } from 'react';
import { subscribeMultiDrag } from './multiDragLiveChannel';
// The one consumer of multiDragLiveChannel: style-writes `translate` on co-selected card roots
// per frame. CSS `translate` composes with `transform`, so framer-motion's scale/spring inline
// transforms are never clobbered (the transform-trap that bit the minimized-cards work).
export function useLiveMultiDrag(): void {
useEffect(() => {
const els = new Map<string, HTMLElement | null>();
return subscribeMultiDrag((update) => {
if (!update) {
for (const el of els.values()) { if (el) el.style.translate = ''; }
els.clear();
return;
}
const value = `${update.dx}px ${update.dy}px`;
for (const id of update.ids) {
let el = els.get(id);
if (el === undefined) {
el = document.querySelector<HTMLElement>(`[data-osw-card="${CSS.escape(id)}"]`);
els.set(id, el);
}
if (el) el.style.translate = value;
}
});
}, []);
}
@@ -104,7 +104,7 @@ export function useDashboardController(dashboardId: string, isActive: boolean) {
}), [canvas.actions]);
const {
multiDragDelta,
multiDragActive,
handleCardDragStart,
handleCardDragMove,
handleCardDragEnd,
@@ -342,7 +342,7 @@ export function useDashboardController(dashboardId: string, isActive: boolean) {
cards, viewCards, browserCards, keepAliveBrowserCards, outputs, glowingAgentCards,
workflowCards, workflowsHub,
expandedSessionIds, tetherInputs, highlightedCardId, autoFocusSessionId,
focusedCardId, multiDragDelta, shakeDirection,
focusedCardId, multiDragActive, shakeDirection,
neighborDirections, toolbarOpen, searchPaletteOpen, newAgentBounce, canvasEmpty,
toolbarRef, spawnOriginsRef, revealSpawnedRef, measuredHeightsRef, getCanvasState,
toolbarPrefill,
@@ -26,7 +26,7 @@ interface Props {
getCanvasState: () => { panX: number; panY: number; zoom: number };
isSelected?: boolean;
isHighlighted?: boolean;
multiDragDelta?: { dx: number; dy: number } | null;
multiDragActive?: boolean;
onCardSelect?: (id: string, type: CardType, shiftKey: boolean) => void;
onDragStart?: (id: string, type: CardType) => void;
onDragMove?: (dx: number, dy: number, mouseX?: number, mouseY?: number) => void;
@@ -38,7 +38,7 @@ interface Props {
const MarketplaceAppCard: React.FC<Props> = ({
cardX, cardY, cardWidth, cardHeight, cardZOrder = 0,
getCanvasState,
isSelected = false, isHighlighted = false, multiDragDelta = null,
isSelected = false, isHighlighted = false, multiDragActive = false,
onCardSelect, onDragStart, onDragMove, onDragEnd, onBringToFront,
}) => {
const c = useClaudeTokens();
@@ -73,7 +73,7 @@ const MarketplaceAppCard: React.FC<Props> = ({
getCanvasState={getCanvasState}
isSelected={isSelected}
isHighlighted={isHighlighted}
multiDragDelta={multiDragDelta}
multiDragActive={multiDragActive}
onCardSelect={onCardSelect}
onDragStart={onDragStart}
onDragMove={onDragMove}
@@ -27,7 +27,7 @@ interface Props {
getCanvasState: () => { panX: number; panY: number; zoom: number };
isSelected?: boolean;
isHighlighted?: boolean;
multiDragDelta?: { dx: number; dy: number } | null;
multiDragActive?: boolean;
onCardSelect?: (id: string, type: CardType, shiftKey: boolean) => void;
onDragStart?: (id: string, type: CardType) => void;
onDragMove?: (dx: number, dy: number, mouseX?: number, mouseY?: number) => void;
@@ -39,7 +39,7 @@ interface Props {
const SettingsAppCard: React.FC<Props> = ({
cardX, cardY, cardWidth, cardHeight, cardZOrder = 0,
getCanvasState,
isSelected = false, isHighlighted = false, multiDragDelta = null,
isSelected = false, isHighlighted = false, multiDragActive = false,
onCardSelect, onDragStart, onDragMove, onDragEnd, onBringToFront,
}) => {
const c = useClaudeTokens();
@@ -75,7 +75,7 @@ const SettingsAppCard: React.FC<Props> = ({
getCanvasState={getCanvasState}
isSelected={isSelected}
isHighlighted={isHighlighted}
multiDragDelta={multiDragDelta}
multiDragActive={multiDragActive}
onCardSelect={onCardSelect}
onDragStart={onDragStart}
onDragMove={onDragMove}
@@ -18,7 +18,7 @@ interface Props {
getCanvasState: () => { panX: number; panY: number; zoom: number };
isSelected?: boolean;
isHighlighted?: boolean;
multiDragDelta?: { dx: number; dy: number } | null;
multiDragActive?: boolean;
onCardSelect?: (id: string, type: CardType, shiftKey: boolean) => void;
onDragStart?: (id: string, type: CardType) => void;
onDragMove?: (dx: number, dy: number, mouseX?: number, mouseY?: number) => void;
@@ -29,7 +29,7 @@ interface Props {
const WorkflowsAppCard: React.FC<Props> = ({
cardX, cardY, cardWidth, cardHeight, cardZOrder = 0,
getCanvasState,
isSelected = false, isHighlighted = false, multiDragDelta = null,
isSelected = false, isHighlighted = false, multiDragActive = false,
onCardSelect, onDragStart, onDragMove, onDragEnd, onBringToFront,
}) => {
const WC = useWC();
@@ -67,7 +67,7 @@ const WorkflowsAppCard: React.FC<Props> = ({
getCanvasState={getCanvasState}
isSelected={isSelected}
isHighlighted={isHighlighted}
multiDragDelta={multiDragDelta}
multiDragActive={multiDragActive}
onCardSelect={onCardSelect}
onDragStart={onDragStart}
onDragMove={onDragMove}