diff --git a/plugins/kanban/dashboard/dist/index.js b/plugins/kanban/dashboard/dist/index.js index d932bb1d24f..a801a87327c 100644 --- a/plugins/kanban/dashboard/dist/index.js +++ b/plugins/kanban/dashboard/dist/index.js @@ -2273,6 +2273,93 @@ // ------------------------------------------------------------------------- function BoardColumns(props) { + const columnsRef = useRef(null); + const panRef = useRef({ isPanning: false, startX: 0, scrollLeft: 0 }); + const [isPanning, setIsPanning] = useState(false); + const [isScrollable, setIsScrollable] = useState(false); + + const checkScrollable = useCallback(function () { + const el = columnsRef.current; + setIsScrollable(!!el && el.scrollWidth > el.clientWidth + 1); + }, []); + + useEffect(function () { + checkScrollable(); + const el = columnsRef.current; + if (!el) return undefined; + if (typeof ResizeObserver !== "undefined") { + const observer = new ResizeObserver(checkScrollable); + observer.observe(el); + return function () { observer.disconnect(); }; + } + window.addEventListener("resize", checkScrollable); + return function () { window.removeEventListener("resize", checkScrollable); }; + }, [checkScrollable, props.board]); + + const isPanBlockedTarget = useCallback(function (target) { + if (!target) return true; + if (target.closest && target.closest(".hermes-kanban-card")) return true; + if (target.closest && target.closest(".hermes-kanban-column-add")) return true; + if (target.closest && target.closest(".hermes-kanban-col-check")) return true; + if (target.closest && target.closest("button,input,textarea,select,a,[role='button']")) return true; + return false; + }, []); + + const stopPan = useCallback(function () { + const el = columnsRef.current; + if (!panRef.current.isPanning) return; + panRef.current.isPanning = false; + setIsPanning(false); + if (el) { + // Keep cursor feedback instant even before React flushes the state update. + el.classList.remove("hermes-kanban-columns--panning"); + el.style.userSelect = ""; + } + if (panRef.current.cleanup) panRef.current.cleanup(); + panRef.current.cleanup = null; + }, []); + + useEffect(function () { + return function () { stopPan(); }; + }, [stopPan]); + + const handleMouseDown = useCallback(function (e) { + if (e.button !== 0) return; + if (isPanBlockedTarget(e.target)) return; + const el = columnsRef.current; + if (!el) return; + const rect = el.getBoundingClientRect(); + // Preserve the native horizontal scrollbar as a fallback; grab-pan starts above it. + if (e.clientY >= rect.bottom - 20) return; + if (el.scrollWidth <= el.clientWidth) return; + + panRef.current.isPanning = true; + panRef.current.startX = e.clientX; + panRef.current.scrollLeft = el.scrollLeft; + setIsPanning(true); + el.classList.add("hermes-kanban-columns--panning"); + el.style.userSelect = "none"; + + function onMouseMove(ev) { + if (!panRef.current.isPanning) return; + const dx = ev.clientX - panRef.current.startX; + el.scrollLeft = panRef.current.scrollLeft - dx; + ev.preventDefault(); + } + function onMouseUp() { stopPan(); } + + if (panRef.current.cleanup) panRef.current.cleanup(); + window.addEventListener("mousemove", onMouseMove); + window.addEventListener("mouseup", onMouseUp, { once: true }); + window.addEventListener("blur", onMouseUp, { once: true }); + panRef.current.cleanup = function () { + window.removeEventListener("mousemove", onMouseMove); + window.removeEventListener("mouseup", onMouseUp); + window.removeEventListener("blur", onMouseUp); + }; + e.preventDefault(); + }, [isPanBlockedTarget, stopPan]); + const handleDragStart = useCallback(function (e) { const card = e.target.closest && e.target.closest(".hermes-kanban-card"); if (!card) return; @@ -2282,7 +2369,17 @@ const handleDragEnd = useCallback(function () { if (props.onDragEnd) props.onDragEnd(); }, [props.onDragEnd]); - return h("div", { className: "hermes-kanban-columns", onDragStart: handleDragStart, onDragEnd: handleDragEnd }, + return h("div", { + ref: columnsRef, + className: cn( + "hermes-kanban-columns", + isScrollable ? "hermes-kanban-columns--scrollable" : "", + isPanning ? "hermes-kanban-columns--panning" : "", + ), + onDragStart: handleDragStart, + onDragEnd: handleDragEnd, + onMouseDown: handleMouseDown, + }, props.board.columns.map(function (col) { return h(Column, { key: col.name, diff --git a/plugins/kanban/dashboard/dist/style.css b/plugins/kanban/dashboard/dist/style.css index 6b396b2612e..8f2a43c295f 100644 --- a/plugins/kanban/dashboard/dist/style.css +++ b/plugins/kanban/dashboard/dist/style.css @@ -69,6 +69,15 @@ overflow-x: auto; } +.hermes-kanban-columns--scrollable { + cursor: grab; +} + +.hermes-kanban-columns--panning, +.hermes-kanban-columns--panning * { + cursor: grabbing !important; +} + .hermes-kanban-column { flex: 0 0 280px; display: flex;