feat(FN-4574): complete Step 2 — add mobile board stabilization effect

Fusion-Task-Id: FN-4574
Fusion-Task-Lineage: 6092cde6-b60b-4f7b-ac46-ceb730970eb2
This commit is contained in:
Fusion
2026-05-15 01:06:59 -07:00
committed by gsxdsm
parent a7bd9f41d1
commit 80c731b5da

View File

@@ -76,6 +76,7 @@ export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onPauseTask
const [archivedCollapsed, setArchivedCollapsed] = useState(true);
const archivedLoadedRef = useRef(false);
const [workflowStepNameLookup, setWorkflowStepNameLookup] = useState<ReadonlyMap<string, string>>(EMPTY_WORKFLOW_STEP_NAME_LOOKUP);
const boardRef = useRef<HTMLElement | null>(null);
const blockerFanoutMap = useBlockerFanout(tasks, {
staleHighFanoutAgeThresholdMs: staleHighFanoutBlockerAgeThresholdMs,
});
@@ -163,6 +164,76 @@ export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onPauseTask
// We keep the FN-001 baseline (`scroll-snap-type: x proximity` +
// `overflow-anchor: none`) and only stabilize via reflow + scroll offset
// normalization; do NOT reintroduce `scroll-snap-type: x mandatory`.
useEffect(() => {
if (!window.matchMedia("(max-width: 768px)").matches) {
return;
}
let rafId: number | null = null;
let timeoutId: ReturnType<typeof setTimeout> | null = null;
const runStabilization = () => {
const boardEl = boardRef.current;
if (!boardEl) return;
void boardEl.offsetWidth;
boardEl.scrollLeft = 0;
};
const scheduleStabilization = () => {
if (typeof window.requestAnimationFrame === "function") {
if (rafId !== null) {
window.cancelAnimationFrame(rafId);
}
rafId = window.requestAnimationFrame(() => {
rafId = null;
runStabilization();
});
return;
}
if (timeoutId !== null) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
timeoutId = null;
runStabilization();
}, 0);
};
const handlePageShow = (event: PageTransitionEvent) => {
const viewportScale = window.visualViewport?.scale ?? 1;
if (event.persisted || viewportScale > 1.0001) {
scheduleStabilization();
}
};
scheduleStabilization();
window.addEventListener("pageshow", handlePageShow);
const visualViewport = window.visualViewport;
let handleViewportResize: (() => void) | null = null;
if (visualViewport) {
handleViewportResize = () => {
scheduleStabilization();
visualViewport.removeEventListener("resize", handleViewportResize!);
handleViewportResize = null;
};
visualViewport.addEventListener("resize", handleViewportResize);
}
return () => {
window.removeEventListener("pageshow", handlePageShow);
if (handleViewportResize) {
visualViewport?.removeEventListener("resize", handleViewportResize);
}
if (rafId !== null) {
window.cancelAnimationFrame(rafId);
}
if (timeoutId !== null) {
clearTimeout(timeoutId);
}
};
}, []);
// FN-4380: GitHub badge state comes from persisted task fields (`task.prInfo`,
// `task.issueInfo`, `task.githubTracking.issue`) and live WebSocket `badge:updated`
@@ -170,7 +241,7 @@ export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onPauseTask
return (
<>
<main className="board" id="board">
<main className="board" id="board" ref={boardRef}>
{COLUMNS.map((col) => (
<Column
key={col}