mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-08 13:12:08 +00:00
feat(kanban): add grab-to-pan board scrolling
Adds grab-to-pan horizontal scrolling to the Kanban dashboard board columns with grab/grabbing cursor feedback. Card drag-and-drop, add buttons, checkboxes, links, and inputs are excluded from panning, the native horizontal scrollbar remains usable as a fallback, and panning state is cleared on mouse release or window blur. Salvaged from PR #59830 by @vampyren (original commit authored under an unlinked local git identity; rewritten to their GitHub noreply address to preserve attribution).
This commit is contained in:
parent
afb5808d8c
commit
cc0aa18fe7
2 changed files with 107 additions and 1 deletions
99
plugins/kanban/dashboard/dist/index.js
vendored
99
plugins/kanban/dashboard/dist/index.js
vendored
|
|
@ -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,
|
||||
|
|
|
|||
9
plugins/kanban/dashboard/dist/style.css
vendored
9
plugins/kanban/dashboard/dist/style.css
vendored
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue