// Drag-and-drop state + undo toast for the Overview status grid. // // The drag state lives on `window` (not React) because HTML5 drag events // fire on DOM nodes that don't always re-render during the drag; forcing // everything through React state would cause the drop-zone highlight to // lag behind the cursor. Instead, components subscribe via a custom event // and a tiny `useDragState` hook that tracks whether *a* drag is active. // // The undo toast is a global overlay; `window.__pulseShowUndo({label, onUndo})` // pushes a new toast that auto-dismisses after 5 seconds. (function () { // --- drag state singleton -------------------------------------------------- window.__pulseDnD = window.__pulseDnD || { activeNodeId: null, sourceGroup: null }; function setDragActive(nodeId, sourceGroup) { window.__pulseDnD = { activeNodeId: nodeId, sourceGroup }; window.dispatchEvent(new CustomEvent('pulse:dnd-change')); } function clearDragActive() { window.__pulseDnD = { activeNodeId: null, sourceGroup: null }; window.dispatchEvent(new CustomEvent('pulse:dnd-change')); } function useDragState() { const [state, setState] = React.useState(window.__pulseDnD); React.useEffect(() => { function onChange() { setState({ ...window.__pulseDnD }); } window.addEventListener('pulse:dnd-change', onChange); return () => window.removeEventListener('pulse:dnd-change', onChange); }, []); return state; } // --- undo-toast stack ------------------------------------------------------ // Each toast: { id, label, onUndo, createdAt } // Stored on window so multiple components (overview, future views) can push. window.__pulseToasts = window.__pulseToasts || []; let toastSeq = 0; function showUndoToast({ label, onUndo, ttlMs = 5000 }) { const id = ++toastSeq; const entry = { id, label, onUndo, createdAt: Date.now(), ttlMs }; // Cap at 3 visible so fast repeated moves don't flood the screen. window.__pulseToasts = [entry, ...window.__pulseToasts].slice(0, 3); window.dispatchEvent(new CustomEvent('pulse:toasts-change')); setTimeout(() => dismissToast(id), ttlMs); } function dismissToast(id) { const before = window.__pulseToasts.length; window.__pulseToasts = window.__pulseToasts.filter(t => t.id !== id); if (window.__pulseToasts.length !== before) { window.dispatchEvent(new CustomEvent('pulse:toasts-change')); } } function UndoToastStack() { // All hooks above any early return. const [toasts, setToasts] = React.useState(window.__pulseToasts); React.useEffect(() => { function onChange() { setToasts([...window.__pulseToasts]); } window.addEventListener('pulse:toasts-change', onChange); // Session-lost → clear toasts so they don't leak across logins. function onSessionLost() { window.__pulseToasts = []; setToasts([]); } window.addEventListener('pulse:session-lost', onSessionLost); return () => { window.removeEventListener('pulse:toasts-change', onChange); window.removeEventListener('pulse:session-lost', onSessionLost); }; }, []); if (!toasts.length) return null; return (
{toasts.map(t => (
{t.label}
))}
); } // Expose to window so overview.jsx and app.jsx can use them. Object.assign(window, { setDragActive, clearDragActive, useDragState, __pulseShowUndo: showUndoToast, UndoToastStack, }); })();