test
This commit is contained in:
@@ -35,28 +35,47 @@ export function Canvas({ initialCards }: CanvasProps) {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onWheel = useCallback(
|
||||
(e: React.WheelEvent) => {
|
||||
if (!e.ctrlKey && !e.metaKey) return;
|
||||
e.preventDefault();
|
||||
const rect = containerRef.current!.getBoundingClientRect();
|
||||
const mx = e.clientX - rect.left;
|
||||
const my = e.clientY - rect.top;
|
||||
setVp((prev) => {
|
||||
const factor = Math.exp(-e.deltaY * ZOOM_SENSITIVITY);
|
||||
const next = Math.max(MIN_SCALE, Math.min(MAX_SCALE, prev.scale * factor));
|
||||
const k = next / prev.scale;
|
||||
return { x: mx - (mx - prev.x) * k, y: my - (my - prev.y) * k, scale: next };
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
// Suppress WebKit/GTK middle-click defaults (autoscroll on some platforms,
|
||||
// paste primary selection on Linux). Must be a native listener — React's
|
||||
// synthetic onPointerDown.preventDefault doesn't reliably stop these.
|
||||
useEffect(() => {
|
||||
const el = containerRef.current;
|
||||
if (!el) return;
|
||||
const stopMiddle = (e: MouseEvent) => {
|
||||
if (e.button === 1) e.preventDefault();
|
||||
};
|
||||
const stopAux = (e: MouseEvent) => {
|
||||
if (e.button === 1) e.preventDefault();
|
||||
};
|
||||
el.addEventListener("mousedown", stopMiddle);
|
||||
el.addEventListener("auxclick", stopAux);
|
||||
return () => {
|
||||
el.removeEventListener("mousedown", stopMiddle);
|
||||
el.removeEventListener("auxclick", stopAux);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onWheel = useCallback((e: React.WheelEvent) => {
|
||||
if (!e.ctrlKey && !e.metaKey) return;
|
||||
e.preventDefault();
|
||||
const rect = containerRef.current!.getBoundingClientRect();
|
||||
const mx = e.clientX - rect.left;
|
||||
const my = e.clientY - rect.top;
|
||||
setVp((prev) => {
|
||||
const factor = Math.exp(-e.deltaY * ZOOM_SENSITIVITY);
|
||||
const next = Math.max(MIN_SCALE, Math.min(MAX_SCALE, prev.scale * factor));
|
||||
const k = next / prev.scale;
|
||||
return { x: mx - (mx - prev.x) * k, y: my - (my - prev.y) * k, scale: next };
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onPointerDown = (e: React.PointerEvent) => {
|
||||
const isPan = e.button === 1 || (e.button === 0 && spaceHeld);
|
||||
if (!isPan) return;
|
||||
e.preventDefault();
|
||||
(e.target as Element).setPointerCapture(e.pointerId);
|
||||
// Capture on the container, not e.target — survives card re-renders and
|
||||
// avoids odd behavior when the click lands on a textarea or child element.
|
||||
containerRef.current?.setPointerCapture(e.pointerId);
|
||||
panState.current = { startX: e.clientX, startY: e.clientY, vpX: vp.x, vpY: vp.y };
|
||||
};
|
||||
|
||||
@@ -69,7 +88,11 @@ export function Canvas({ initialCards }: CanvasProps) {
|
||||
|
||||
const onPointerUp = (e: React.PointerEvent) => {
|
||||
if (panState.current) {
|
||||
(e.target as Element).releasePointerCapture(e.pointerId);
|
||||
try {
|
||||
containerRef.current?.releasePointerCapture(e.pointerId);
|
||||
} catch {
|
||||
// capture may already be released if pointercancel fired
|
||||
}
|
||||
panState.current = null;
|
||||
}
|
||||
};
|
||||
@@ -81,7 +104,7 @@ export function Canvas({ initialCards }: CanvasProps) {
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={`canvas-container ${spaceHeld ? "pan-mode" : ""}`}
|
||||
className={`canvas-container ${spaceHeld ? "pan-mode" : ""} ${panState.current ? "panning" : ""}`}
|
||||
onWheel={onWheel}
|
||||
onPointerDown={onPointerDown}
|
||||
onPointerMove={onPointerMove}
|
||||
|
||||
@@ -4,12 +4,18 @@
|
||||
overflow: hidden;
|
||||
cursor: default;
|
||||
background: var(--bg);
|
||||
touch-action: none;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.canvas-container.pan-mode {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.canvas-container.panning {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.canvas-grid {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
|
||||
Reference in New Issue
Block a user