[eric] canvas: revert drag-to-edge snap (card-disappear bug on the real drag)

This commit is contained in:
ciregenz
2026-07-21 22:25:48 -07:00
parent 8a482ce029
commit 02ba9db33b
4 changed files with 4 additions and 96 deletions
@@ -7,7 +7,6 @@ import TetherLayer from './TetherLayer';
import DashboardCardLayer from './DashboardCardLayer';
import DashboardOverlays from './DashboardOverlays';
import DashboardEmptyState from './DashboardEmptyState';
import SnapZonePreview from './SnapZonePreview';
import '../desktop/desktop.css';
import DesktopDock from '../desktop/DesktopDock';
import MinimizedStack from '../desktop/MinimizedStack';
@@ -62,7 +61,6 @@ interface DashboardCanvasProps {
focusedCardId: string | null;
pendingFocusNoteId: string | null;
multiDragDelta: { dx: number; dy: number } | null;
snapZone: string | null;
shakeDirection: Direction | null;
neighborDirections: NeighborDirections;
toolbarOpen: boolean;
@@ -126,7 +124,6 @@ const DashboardCanvas: React.FC<DashboardCanvasProps> = ({
focusedCardId,
pendingFocusNoteId,
multiDragDelta,
snapZone,
shakeDirection,
neighborDirections,
toolbarOpen,
@@ -268,8 +265,6 @@ const DashboardCanvas: React.FC<DashboardCanvasProps> = ({
/>
)}
<SnapZonePreview zone={snapZone} />
{!anyFullscreen && (
<DesktopDock
sessions={sessions}
@@ -1,43 +0,0 @@
import React from 'react';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
import { TILE_ZONES } from '@/app/pages/Dashboard/cards/tileZones';
// A translucent ghost of where a dragged card will land once released against the edge. Screen-space
// (fixed), so it lives outside the pan/zoom layer and never counter-transforms. 'fullscreen' fills
// the whole viewport; every other zone is a fraction of it.
const GAP = 8;
interface SnapZonePreviewProps {
zone: string | null;
}
const SnapZonePreview: React.FC<SnapZonePreviewProps> = ({ zone }) => {
const c = useClaudeTokens();
if (!zone) return null;
const vp = document.querySelector('[data-canvas-viewport]');
const rect = vp ? vp.getBoundingClientRect() : { left: 0, top: 0, width: window.innerWidth, height: window.innerHeight };
const frac = zone === 'fullscreen' ? { x: 0, y: 0, w: 1, h: 1 } : TILE_ZONES[zone];
if (!frac) return null;
return (
<div
style={{
position: 'fixed',
left: rect.left + frac.x * rect.width + GAP,
top: rect.top + frac.y * rect.height + GAP,
width: frac.w * rect.width - GAP * 2,
height: frac.h * rect.height - GAP * 2,
borderRadius: 14,
background: `${c.accent.primary}22`,
border: `2px solid ${c.accent.primary}`,
boxShadow: `0 0 0 100vmax ${c.accent.primary}0A`,
pointerEvents: 'none',
zIndex: 999985,
transition: 'left 0.12s ease, top 0.12s ease, width 0.12s ease, height 0.12s ease',
}}
/>
);
};
export default SnapZonePreview;
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useRef, useState, type RefObject } from 'react';
import { report } from '@/shared/serviceClient';
import { useAppDispatch } from '@/shared/hooks';
import { moveCards, setTiledCard } from '@/shared/state/dashboardLayoutSlice';
import { moveCards } from '@/shared/state/dashboardLayoutSlice';
import type { CardType, useDashboardSelection } from '../state/useDashboardSelection';
import type { CanvasActions } from './useCanvasControls';
@@ -15,25 +15,6 @@ interface UseCardDragArgs {
const EDGE_ZONE = 60;
const EDGE_MAX_SPEED = 8;
// Jam a dragged card into the very edge (inside the pan zone) to snap-tile it. Grazing the 60px
// edge still pans the infinite canvas; only the inner 24px band arms a snap and pauses the pan, so
// the two never fight. Top = fullscreen (macOS drag-to-top), sides = halves, corners = quarters.
const SNAP_BAND = 24;
function snapZoneFor(mx: number, my: number, rect: DOMRect): string | null {
const nearL = mx <= rect.left + SNAP_BAND;
const nearR = mx >= rect.right - SNAP_BAND;
const nearT = my <= rect.top + SNAP_BAND;
const nearB = my >= rect.bottom - SNAP_BAND;
if (nearT && nearL) return 'tl';
if (nearT && nearR) return 'tr';
if (nearB && nearL) return 'bl';
if (nearB && nearR) return 'br';
if (nearT) return 'fullscreen';
if (nearL) return 'left';
if (nearR) return 'right';
return null; // bottom-center is left alone: the composer + dock live there.
}
// Clamped: an infinite canvas lets the cursor sit arbitrarily far outside the viewport, where an unclamped ramp would scale pan speed with distance instead of saturating.
function axisIntensity(pos: number, lo: number, hi: number): number {
@@ -51,10 +32,6 @@ export function useCardDrag({
const [multiDragDelta, setMultiDragDelta] = useState<{ dx: number; dy: number } | null>(null);
const [liveDragInfo, setLiveDragInfo] = useState<{ cardId: string; dx: number; dy: number } | null>(null);
const [snapZone, setSnapZone] = useState<string | null>(null);
const snapZoneRef = useRef<string | null>(null);
// Snap only a lone card: dragging one of several selected cards to the edge shouldn't tile just it.
const snapEligibleRef = useRef(true);
const activeDragCardRef = useRef<string | null>(null);
const isMultiDragRef = useRef(false);
@@ -80,8 +57,7 @@ export function useCardDrag({
const dx = EDGE_MAX_SPEED * axisIntensity(mx, rect.left, rect.right);
const dy = EDGE_MAX_SPEED * axisIntensity(my, rect.top, rect.bottom);
// Snap armed = the card is about to tile at this edge, so freeze the canvas instead of panning.
if ((dx !== 0 || dy !== 0) && !snapZoneRef.current) {
if (dx !== 0 || dy !== 0) {
// Live-only write (no React commit per frame); clearDrag commits once when the drag ends.
canvasActions.panBy(dx, dy);
}
@@ -91,7 +67,6 @@ export function useCardDrag({
const handleCardDragStart = useCallback((id: string, type: CardType) => {
activeDragCardRef.current = id;
snapEligibleRef.current = selection.selectedArray().filter((s) => s.id !== id).length === 0;
if (selection.isSelected(id)) {
isMultiDragRef.current = true;
} else {
@@ -105,16 +80,6 @@ export function useCardDrag({
if (mouseX !== undefined && mouseY !== undefined) {
lastMousePosRef.current = { x: mouseX, y: mouseY };
}
// Arm/clear the snap target from the live cursor. Single-card drags only: snapping one card of a
// multi-selection while the rest move is incoherent.
const vp = viewportRef.current;
const nextSnap = (vp && snapEligibleRef.current && mouseX !== undefined && mouseY !== undefined)
? snapZoneFor(mouseX, mouseY, vp.getBoundingClientRect())
: null;
if (nextSnap !== snapZoneRef.current) {
snapZoneRef.current = nextSnap;
setSnapZone(nextSnap);
}
// Arm the webview shield on the first real MOVE, not on pointerdown: a plain click also arms the drag machinery, and shielding then made the click-to-focus camera fit skip (it saw a "drag in progress"), so focusing a card took two clicks. On a real drag the shield still goes up before the pointer travels, so the webview neutralization + no-nudge + release-over-webview fixes all hold. Idempotent add.
document.body.classList.add('dashboard-marquee-active');
// Start edge panning only once actual dragging begins; a live frame handle means the loop is already running.
@@ -134,8 +99,6 @@ export function useCardDrag({
// Reconcile React with whatever edge-pan wrote live during the drag.
canvasActions.commit();
activeDragCardRef.current = null;
snapZoneRef.current = null;
setSnapZone(null);
document.body.classList.remove('dashboard-marquee-active');
isMultiDragRef.current = false;
setMultiDragDelta(null);
@@ -144,12 +107,7 @@ export function useCardDrag({
const handleCardDragEnd = useCallback((dx: number, dy: number, didDrag: boolean) => {
if (didDrag) report('dashboard', 'card_dragged');
const snap = snapZoneRef.current;
const activeId = activeDragCardRef.current;
if (snap && didDrag && activeId) {
// Released against the edge: tile the card there instead of leaving it at the drop position.
dispatch(setTiledCard({ cardId: activeId, zone: snap }));
} else if (isMultiDragRef.current && didDrag) {
if (isMultiDragRef.current && didDrag) {
const items = selection.selectedArray()
.filter((s) => s.id !== activeDragCardRef.current);
if (items.length > 0) {
@@ -176,7 +134,6 @@ export function useCardDrag({
return {
multiDragDelta,
liveDragInfo,
snapZone,
handleCardDragStart,
handleCardDragMove,
handleCardDragEnd,
@@ -103,7 +103,6 @@ export function useDashboardController(dashboardId: string, isActive: boolean) {
const {
multiDragDelta,
liveDragInfo,
snapZone,
handleCardDragStart,
handleCardDragMove,
handleCardDragEnd,
@@ -333,7 +332,7 @@ export function useDashboardController(dashboardId: string, isActive: boolean) {
cards, viewCards, browserCards, keepAliveBrowserCards, notes, outputs, glowingAgentCards,
workflowCards, workflowsHub,
expandedSessionIds, tethers, highlightedCardId, autoFocusSessionId,
focusedCardId, pendingFocusNoteId, multiDragDelta, snapZone, shakeDirection,
focusedCardId, pendingFocusNoteId, multiDragDelta, shakeDirection,
neighborDirections, toolbarOpen, searchPaletteOpen, newAgentBounce,
toolbarRef, spawnOriginsRef, revealSpawnedRef, measuredHeightsRef, getCanvasState,
toolbarPrefill,