Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 98 additions & 1 deletion plugins/kanban/dashboard/dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand Down
9 changes: 9 additions & 0 deletions plugins/kanban/dashboard/dist/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions scripts/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@

# Auto-extracted from noreply emails + manual overrides
AUTHOR_MAP = {
"4087127+vampyren@users.noreply.github.com": "vampyren", # PR #59830 salvage (kanban: grab-to-pan board scrolling; original commit under unlinked local identity)
"spiky02plateau@users.noreply.github.com": "spiky02plateau", # PR #32824 salvage (usage: fetch Codex account limits from the credential pool in pool-only setups; superseded by #60028)
"taylorhp@gmail.com": "hwrdprkns", # PR #36896 salvage (secrets: 1Password op:// secret source + shared _cache substrate, adapted onto the SecretSource interface)
"ishengeqi@163.com": "isheng-eqi", # PR #59428 salvage (cron: reject past one-shot timestamps in update_job fallback + resume_job; #59395). Also PR #59446 salvage (cron: advance one-shot next_run_at before dispatch so concurrent gateway+desktop schedulers can't double-execute; #59229).
Expand Down
Loading