diff --git a/frontend/src/components/strategyEditor/FlowNodes.tsx b/frontend/src/components/strategyEditor/FlowNodes.tsx
index cd3712f..dd731f0 100644
--- a/frontend/src/components/strategyEditor/FlowNodes.tsx
+++ b/frontend/src/components/strategyEditor/FlowNodes.tsx
@@ -129,6 +129,12 @@ function MultiSideHandles({
function usePaletteDropHandlers(id: string, d: StrategyFlowNodeData) {
const { screenToFlowPosition } = useReactFlow();
+ const onDragEnter = useCallback((e: React.DragEvent) => {
+ if (!isPaletteHtmlDrag(e)) return;
+ e.preventDefault();
+ e.stopPropagation();
+ }, []);
+
const onDragOver = useCallback((e: React.DragEvent) => {
if (!isPaletteHtmlDrag(e)) return;
e.preventDefault();
@@ -154,13 +160,13 @@ function usePaletteDropHandlers(id: string, d: StrategyFlowNodeData) {
d.onDropTarget?.(id, payload, flowPos);
}, [d, id, screenToFlowPosition]);
- return { onDragOver, onDragLeave, onDrop };
+ return { onDragEnter, onDragOver, onDragLeave, onDrop };
}
export const StartNode = memo(function StartNode({ id, data }: NodeProps) {
const d = data as StrategyFlowNodeData;
const isSell = d.signalTab === 'sell';
- const { onDragOver, onDragLeave, onDrop } = usePaletteDropHandlers(id, d);
+ const { onDragEnter, onDragOver, onDragLeave, onDrop } = usePaletteDropHandlers(id, d);
const candleTypes = d.candleTypes?.length
? d.candleTypes
: getStartCandleTypes({ candleType: d.candleType ?? '1m' });
@@ -171,6 +177,7 @@ export const StartNode = memo(function StartNode({ id, data }: NodeProps) {
return (
{
+ markPaletteHtmlDragEnd();
+ };
+
const onPointerDown = (e: React.PointerEvent) => {
handlePalettePointerDown(e, buildPayload(), composite ? `${label} + ${label}` : label);
};
@@ -92,6 +97,7 @@ export default function PaletteChip({
draggable={!needsPointerPaletteDrag()}
className={`se-palette-card ${color ? `se-palette-card--${color}` : 'se-palette-card--ind'}${composite ? ' se-palette-card--composite' : ''}${selected ? ' se-palette-card--selected' : ''}`}
onDragStart={onDragStart}
+ onDragEnd={onDragEnd}
onPointerDown={onPointerDown}
onMouseDown={onMouseDown}
onTouchStart={onTouchStart}
diff --git a/frontend/src/components/strategyEditor/SidewaysFilterPaletteTab.tsx b/frontend/src/components/strategyEditor/SidewaysFilterPaletteTab.tsx
index 979cf9d..3a51b9f 100644
--- a/frontend/src/components/strategyEditor/SidewaysFilterPaletteTab.tsx
+++ b/frontend/src/components/strategyEditor/SidewaysFilterPaletteTab.tsx
@@ -4,6 +4,7 @@ import type { DefType } from '../../utils/strategyEditorShared';
import {
handlePalettePointerDown,
handlePaletteTouchStart,
+ markPaletteHtmlDragEnd,
needsPointerPaletteDrag,
writePaletteHtmlDragData,
type PaletteDragPayload,
@@ -85,6 +86,11 @@ function SidewaysFilterChip({
writePaletteHtmlDragData(e, payload as PaletteDragPayload);
};
+ const onDragEnd = () => {
+ hideHint();
+ markPaletteHtmlDragEnd();
+ };
+
const onPointerDown = (e: React.PointerEvent) => {
hideHint();
const payload: PaletteDragPayload = {
@@ -123,6 +129,7 @@ function SidewaysFilterChip({
draggable={!needsPointerPaletteDrag()}
className={`se-palette-card se-palette-card--range se-palette-card--range-${item.mode}${selected ? ' se-palette-card--selected' : ''}`}
onDragStart={onDragStart}
+ onDragEnd={onDragEnd}
onPointerDown={onPointerDown}
onTouchStart={onTouchStart}
onClick={handleClick}
diff --git a/frontend/src/components/strategyEditor/StrategyEditorCanvas.tsx b/frontend/src/components/strategyEditor/StrategyEditorCanvas.tsx
index d60721c..e6d156e 100644
--- a/frontend/src/components/strategyEditor/StrategyEditorCanvas.tsx
+++ b/frontend/src/components/strategyEditor/StrategyEditorCanvas.tsx
@@ -32,6 +32,8 @@ import {
import { buildSidewaysFilterNode } from '../../utils/sidewaysFilterPaletteStorage';
import {
isPaletteHtmlDrag,
+ isPaletteHtmlDragActive,
+ markPaletteHtmlDragEnd,
needsPointerPaletteDrag,
readPaletteHtmlDragData,
setPaletteDragDropHandler,
@@ -1282,6 +1284,40 @@ function StrategyEditorCanvasInner({
const lastPreviewAnchorRef = useRef(null);
const lastPreviewKeyRef = useRef(null);
+ useEffect(() => {
+ if (needsPointerPaletteDrag()) return;
+ const onDocDragOver = (e: DragEvent) => {
+ if (!isPaletteHtmlDragActive()) return;
+ e.preventDefault();
+ const { flowPos, anchorId } = resolveDropFromClientRef.current(e.clientX, e.clientY);
+ if (anchorId) {
+ const sideKey = `${anchorId}:${Math.round(flowPos.x / 8)}:${Math.round(flowPos.y / 8)}`;
+ if (sideKey === lastPreviewKeyRef.current) return;
+ lastPreviewKeyRef.current = sideKey;
+ lastPreviewAnchorRef.current = anchorId;
+ updatePreviewRef.current(anchorId, flowPos);
+ return;
+ }
+ if (lastPreviewAnchorRef.current != null) {
+ lastPreviewAnchorRef.current = null;
+ lastPreviewKeyRef.current = null;
+ clearPreviewRef.current();
+ }
+ };
+ const onDocDragEnd = () => {
+ lastPreviewAnchorRef.current = null;
+ lastPreviewKeyRef.current = null;
+ clearPreviewRef.current();
+ markPaletteHtmlDragEnd();
+ };
+ document.addEventListener('dragover', onDocDragOver, { passive: false });
+ document.addEventListener('dragend', onDocDragEnd);
+ return () => {
+ document.removeEventListener('dragover', onDocDragOver);
+ document.removeEventListener('dragend', onDocDragEnd);
+ };
+ }, []);
+
useEffect(() => {
if (!needsPointerPaletteDrag()) return;
return subscribePaletteDrag((ev: PaletteDragEvent) => {
@@ -1320,6 +1356,11 @@ function StrategyEditorCanvasInner({
});
}, []);
+ const onPaneDragEnter = useCallback((e: React.DragEvent) => {
+ if (!isPaletteDrag(e)) return;
+ e.preventDefault();
+ }, [isPaletteDrag]);
+
const onPaneDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault();
if (!isPaletteDrag(e)) return;
@@ -1378,6 +1419,7 @@ function StrategyEditorCanvasInner({
isValidConnection={isValidConnection}
onReconnect={onReconnect}
onDrop={onPaneDrop}
+ onDragEnter={onPaneDragEnter}
onDragOver={onPaneDragOver}
onDragLeave={onPaneDragLeave}
connectionMode={ConnectionMode.Loose}
diff --git a/frontend/src/utils/paletteDragSession.ts b/frontend/src/utils/paletteDragSession.ts
index f18690e..7ffb51d 100644
--- a/frontend/src/utils/paletteDragSession.ts
+++ b/frontend/src/utils/paletteDragSession.ts
@@ -54,6 +54,26 @@ let moveRafId: number | null = null;
let pendingMove: { x: number; y: number } | null = null;
let unbindGesture: (() => void) | null = null;
let lastDragClientXY: { x: number; y: number } | null = null;
+let htmlPaletteDragActive = false;
+
+function installHtmlPaletteDragGlobals() {
+ if (typeof window === 'undefined') return;
+ const w = window as Window & { __gcPaletteHtmlDragGlobals?: boolean };
+ if (w.__gcPaletteHtmlDragGlobals) return;
+ w.__gcPaletteHtmlDragGlobals = true;
+
+ window.addEventListener('dragover', (e) => {
+ if (!htmlPaletteDragActive) return;
+ e.preventDefault();
+ if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy';
+ }, { passive: false });
+
+ window.addEventListener('dragend', () => {
+ htmlPaletteDragActive = false;
+ });
+}
+
+installHtmlPaletteDragGlobals();
function hasCoarsePointer(): boolean {
if (typeof window === 'undefined') return false;
@@ -306,7 +326,20 @@ export function handlePaletteTouchStart(
startPaletteDragGesture(touch.clientX, touch.clientY, touch.identifier, payload);
}
+export function markPaletteHtmlDragStart(): void {
+ htmlPaletteDragActive = true;
+}
+
+export function markPaletteHtmlDragEnd(): void {
+ htmlPaletteDragActive = false;
+}
+
+export function isPaletteHtmlDragActive(): boolean {
+ return htmlPaletteDragActive;
+}
+
export function writePaletteHtmlDragData(e: React.DragEvent, payload: PaletteDragPayload): void {
+ markPaletteHtmlDragStart();
const json = JSON.stringify(payload);
e.dataTransfer.setData('application/json', json);
e.dataTransfer.setData('text/plain', json);
@@ -314,13 +347,16 @@ export function writePaletteHtmlDragData(e: React.DragEvent, payload: PaletteDra
}
export function isPaletteHtmlDrag(e: React.DragEvent): boolean {
- return e.dataTransfer.types.includes('application/json')
- || e.dataTransfer.types.includes('text/plain');
+ if (htmlPaletteDragActive) return true;
+ const types = e.dataTransfer?.types;
+ if (!types) return false;
+ return Array.from(types).includes('application/json')
+ || Array.from(types).includes('text/plain');
}
export function readPaletteHtmlDragData(e: React.DragEvent): PaletteDragPayload | null {
- const raw = e.dataTransfer.getData('application/json')
- || e.dataTransfer.getData('text/plain');
+ const raw = e.dataTransfer.getData('text/plain')
+ || e.dataTransfer.getData('application/json');
if (!raw) return null;
try {
return JSON.parse(raw) as PaletteDragPayload;