diff --git a/.changeset/fn-7873-mobile-modal-header-drag.md b/.changeset/fn-7873-mobile-modal-header-drag.md
new file mode 100644
index 0000000000..804d597e67
--- /dev/null
+++ b/.changeset/fn-7873-mobile-modal-header-drag.md
@@ -0,0 +1,7 @@
+---
+"@runfusion/fusion": patch
+---
+
+summary: Restore reliable mobile header dragging for movable floating modals.
+category: fix
+dev: Reasserts the FloatingWindow mobile touch-action contract and covers touch pointer dragging.
diff --git a/packages/dashboard/app/components/FloatingWindow.css b/packages/dashboard/app/components/FloatingWindow.css
index aec8f8ff65..f9a926a8c7 100644
--- a/packages/dashboard/app/components/FloatingWindow.css
+++ b/packages/dashboard/app/components/FloatingWindow.css
@@ -99,6 +99,14 @@ FNXC:ChatModal 2026-06-22-14:49:
On mobile/narrow app viewports, opening Quick Chat should present the full Chat modal as a full-screen sheet instead of a small draggable desktop window. Scope this to the chat FloatingWindow and override the inline desktop geometry only at the mobile breakpoint; desktop pop-out behavior remains movable/resizable.
*/
@media (max-width: 768px) {
+ /*
+ FNXC:FloatingWindow 2026-07-12-17:35:
+ Mobile keeps the global `styles.css` pan-y lockdown so the dashboard cannot drift, but movable FloatingWindow headers must still resolve to an effective `touch-action: none`. Reassert the drag-handle contract at the mobile breakpoint, excluding full-screen sheet variants, so a single-finger header drag stays on the captured pointermove stream instead of being intersected back into page pan by the ancestor chain. Desktop drag/resize and mobile sheet variants are unchanged.
+ */
+ .floating-window:not(.floating-window--chat):not(.floating-window--task-detail):not(.floating-window--workflow-editor):not(.floating-window--automation):not(.floating-window--mission-interview):not(.floating-window--file-browser):not(.floating-window--pr-create):not(.artifacts-gallery-window) .floating-window__header {
+ touch-action: none;
+ }
+
.floating-window--chat {
inset: 0 !important;
width: 100vw !important;
diff --git a/packages/dashboard/app/components/__tests__/FloatingWindow.test.tsx b/packages/dashboard/app/components/__tests__/FloatingWindow.test.tsx
index 9abc7ba0e3..eb5af921ec 100644
--- a/packages/dashboard/app/components/__tests__/FloatingWindow.test.tsx
+++ b/packages/dashboard/app/components/__tests__/FloatingWindow.test.tsx
@@ -1,9 +1,18 @@
import { render, screen, fireEvent } from "@testing-library/react";
import { readFileSync } from "node:fs";
import { beforeEach, describe, expect, it, vi } from "vitest";
+import { loadAllAppCss } from "../../test/cssFixture";
import { FloatingWindow } from "../FloatingWindow";
const floatingWindowCss = readFileSync("app/components/FloatingWindow.css", "utf8");
+const allAppCss = loadAllAppCss();
+
+function cssRuleFor(css: string, selector: string): string {
+ const start = css.indexOf(`${selector} {`);
+ if (start === -1) return "";
+ const end = css.indexOf("}", start);
+ return css.slice(start, end);
+}
/*
FNXC:FloatingWindow 2026-06-22-20:45:
@@ -56,6 +65,56 @@ describe("FloatingWindow", () => {
expect(floatingWindowCss).not.toContain("var(--shadow-xl)");
});
+ it("keeps movable mobile drag handles opted out of the pan-y touch lockdown", () => {
+ expect(allAppCss).toContain("html,");
+ expect(allAppCss).toContain("body {");
+ expect(allAppCss).toContain("touch-action: pan-y;");
+ expect(allAppCss).toContain("* {");
+ expect(allAppCss).toContain("#root {");
+
+ const movableFloatingWindowSelector = ".floating-window:not(.floating-window--chat):not(.floating-window--task-detail):not(.floating-window--workflow-editor):not(.floating-window--automation):not(.floating-window--mission-interview):not(.floating-window--file-browser):not(.floating-window--pr-create):not(.artifacts-gallery-window) .floating-window__header";
+ expect(cssRuleFor(floatingWindowCss, movableFloatingWindowSelector)).toContain("touch-action: none;");
+
+ for (const selector of [
+ ".right-dock-expand-modal__header--draggable",
+ ".terminal-header--draggable",
+ ]) {
+ expect(cssRuleFor(allAppCss, selector)).toContain("touch-action: none;");
+ }
+ });
+
+ it("moves a visible-header window through the captured touch drag path", () => {
+ render(
+ {}}
+ defaultSize={{ width: 320, height: 240 }}
+ defaultPosition={{ x: 80, y: 90 }}
+ minSize={{ width: 240, height: 180 }}
+ >
+ touch drag body
+
+ );
+
+ const panel = screen.getByTestId("floating-window-touch-drag");
+ const header = screen.getByTestId("floating-window-drag-handle-touch-drag");
+ const titleText = screen.getByText(/very long movable floating window title/i);
+ const setPointerCapture = vi.fn();
+ const releasePointerCapture = vi.fn();
+ Object.defineProperty(header, "setPointerCapture", { configurable: true, value: setPointerCapture });
+ Object.defineProperty(header, "releasePointerCapture", { configurable: true, value: releasePointerCapture });
+
+ fireEvent.pointerDown(titleText, { pointerId: 17, pointerType: "touch", clientX: 100, clientY: 120 });
+ fireEvent.pointerMove(header, { pointerId: 17, pointerType: "touch", clientX: 140, clientY: 150 });
+ fireEvent.pointerUp(header, { pointerId: 17, pointerType: "touch", clientX: 140, clientY: 150 });
+
+ expect(setPointerCapture).toHaveBeenCalledWith(17);
+ expect(releasePointerCapture).toHaveBeenCalledWith(17);
+ expect(panel.style.left).toBe("120px");
+ expect(panel.style.top).toBe("120px");
+ });
+
it("can hide generic chrome and delegate dragging to a child header", () => {
render(