diff --git a/packages/dashboard/app/components/NewTaskModal.tsx b/packages/dashboard/app/components/NewTaskModal.tsx
index be8664db1f..197b1a2369 100644
--- a/packages/dashboard/app/components/NewTaskModal.tsx
+++ b/packages/dashboard/app/components/NewTaskModal.tsx
@@ -1,5 +1,6 @@
import "./NewTaskModal.css";
import { useState, useCallback, useEffect, useRef, type CSSProperties, type PointerEvent as ReactPointerEvent } from "react";
+import { createPortal } from "react-dom";
import { useTranslation } from "react-i18next";
import { DEFAULT_TASK_PRIORITY, type Task, type TaskCreateInput, type TaskPriority } from "@fusion/core";
import { getErrorMessage } from "@fusion/core";
@@ -746,7 +747,8 @@ export function NewTaskModal({ isOpen, onClose, projectId, tasks, onCreateTask,
? { left: `${position.x}px`, top: `${position.y}px`, width: `${size.width}px`, height: `${size.height}px`, zIndex }
: keyboardStyle;
- return (
+ // FNXC:FloatingWindow 2026-06-22-22:30: Portaled to document.body so the floating New Task dialog shares the ONE root stacking context with the other floating modals; the shared cross-type z stack only orders correctly at the document root. Mobile sheet is position:fixed, unaffected.
+ return createPortal(
-
+ ,
+ document.body,
);
}
diff --git a/packages/dashboard/app/components/RightDockExpandModal.tsx b/packages/dashboard/app/components/RightDockExpandModal.tsx
index 1fab358995..915d6ed1ee 100644
--- a/packages/dashboard/app/components/RightDockExpandModal.tsx
+++ b/packages/dashboard/app/components/RightDockExpandModal.tsx
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useRef, useState, type CSSProperties, type PointerEvent as ReactPointerEvent, type RefObject } from "react";
+import { createPortal } from "react-dom";
import { Maximize2, X } from "lucide-react";
import { findOverflowViewEntry, type OverflowViewEntry, type OverflowViewKey, type OverflowViewRenderProps, type OverflowViewVisibilityOptions } from "./overflowViewRegistry";
import { nextFloatingZ, currentFloatingZ } from "./floatingWindowStack";
@@ -303,7 +304,8 @@ export function RightDockExpandModal({
zIndex,
} as CSSProperties;
- return (
+ // FNXC:FloatingWindow 2026-06-22-22:30: Portaled to document.body so this floating modal shares the ONE root stacking context with the other floating modals (FloatingWindow/terminal/New Task) — the shared 10100+ z stack only orders correctly across types when they all live at the document root.
+ return createPortal(
+ ,
+ document.body,
);
}
diff --git a/packages/dashboard/app/components/TerminalModal.tsx b/packages/dashboard/app/components/TerminalModal.tsx
index b9ba57a256..f5dacbda71 100644
--- a/packages/dashboard/app/components/TerminalModal.tsx
+++ b/packages/dashboard/app/components/TerminalModal.tsx
@@ -1,4 +1,5 @@
import "./TerminalModal.css";
+import { createPortal } from "react-dom";
import {
useState,
useEffect,
@@ -1823,7 +1824,8 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG
: {}),
} as CSSProperties;
- return (
+ // FNXC:FloatingWindow 2026-06-22-22:30: Portaled to document.body so the terminal shares the ONE root stacking context with the other floating modals; the shared cross-type z stack only orders correctly when all panels live at the document root. Docked/floating/mobile are all position:fixed, so portaling does not change their placement.
+ return createPortal(
-
+ ,
+ document.body,
);
}
diff --git a/packages/dashboard/app/components/__tests__/NewTaskModal.test.tsx b/packages/dashboard/app/components/__tests__/NewTaskModal.test.tsx
index fe05ffc396..17457a03c6 100644
--- a/packages/dashboard/app/components/__tests__/NewTaskModal.test.tsx
+++ b/packages/dashboard/app/components/__tests__/NewTaskModal.test.tsx
@@ -113,8 +113,9 @@ describe("NewTaskModal", () => {
viewportOffsetTop: 50,
});
- const { container } = renderNewTaskModal();
- const modal = container.querySelector(".new-task-modal");
+ renderNewTaskModal();
+ // FNXC: NewTaskModal portals to document.body, so query the modal from document (not the render container).
+ const modal = document.querySelector(".new-task-modal");
expect(mockUseMobileKeyboard).toHaveBeenCalledWith({ enabled: true });
expect(modal?.getAttribute("style")).toContain("--keyboard-overlap: 250px");
diff --git a/packages/dashboard/app/components/__tests__/RightDock.test.tsx b/packages/dashboard/app/components/__tests__/RightDock.test.tsx
index 19ee343600..2c5bbca504 100644
--- a/packages/dashboard/app/components/__tests__/RightDock.test.tsx
+++ b/packages/dashboard/app/components/__tests__/RightDock.test.tsx
@@ -347,16 +347,18 @@ describe("RightDock", () => {
render();
- // Pop out the currently selected (Files) view from the open dock.
+ // Pop out the currently selected (Files) view: the floating modal appears AND
+ // popping out closes the dock (pop-out dismisses the dock so the full-width app
+ // sits behind the movable modal). The dock unmounts; the floating modal survives.
fireEvent.click(screen.getByTestId("right-dock-expand"));
expect(screen.getByTestId("right-dock-expand-modal")).toBeInTheDocument();
-
- // Toggle the dock closed: the dock itself unmounts, the floating modal MUST survive.
- fireEvent.click(screen.getByTestId("harness-toggle-dock"));
expect(screen.queryByTestId("right-dock")).toBeNull();
- expect(screen.getByTestId("right-dock-expand-modal")).toBeInTheDocument();
expect(screen.getByTestId("right-dock-expand-body")).toBeInTheDocument();
+ // Re-opening the dock does not disturb the independent floating modal.
+ fireEvent.click(screen.getByTestId("harness-toggle-dock"));
+ expect(screen.getByTestId("right-dock-expand-modal")).toBeInTheDocument();
+
// Its own close button still dismisses it.
fireEvent.click(screen.getByTestId("right-dock-expand-close"));
expect(screen.queryByTestId("right-dock-expand-modal")).toBeNull();
diff --git a/packages/dashboard/app/components/floatingWindowStack.ts b/packages/dashboard/app/components/floatingWindowStack.ts
index 9fa9aa02d9..d2e8917a48 100644
--- a/packages/dashboard/app/components/floatingWindowStack.ts
+++ b/packages/dashboard/app/components/floatingWindowStack.ts
@@ -2,9 +2,10 @@
FNXC:FloatingWindow 2026-06-22-21:30:
SHARED floating-window z-index stack. This is the ONE source of z-index for every floating modal in the dashboard (FloatingWindow, the right-dock pop-out, the floating terminal, the floating New Task dialog) so they interoperate in a SINGLE stack instead of each type owning a private counter. Previously each modal type managed z-index independently, so tapping e.g. the terminal could not raise it above a popped-out task-detail FloatingWindow. Now every floating modal claims `nextFloatingZ()` on mount/open and again on every panel pointerdown/focus, so the most-recently-interacted window is always on top REGARDLESS of type.
-Base band sits at 4000+ — above ordinary page content and above the base `.modal-overlay` (z-index 100). The counter is module-level and intentionally monotonic: it only ever climbs, which is fine for a session-length dashboard. All floating overlays are `pointer-events: none` (click-through) so raising panels into this shared band never traps clicks on the page behind them.
+FNXC:FloatingWindow 2026-06-22-22:30:
+Base band sits at 10100+ — ABOVE the page overlay/popover band (log viewer, workflow-editor modal, selection popover, fullscreen overlay at z 10000-10001) so a floating window the user is dragging is never painted over by those. Transient top-right toasts are bumped to 10500 (styles.css) so system feedback still shows above a dragged window. The counter is module-level and intentionally monotonic: it only ever climbs, which is fine for a session-length dashboard. All floating overlays are `pointer-events: none` (click-through) so raising panels into this shared band never traps clicks on the page behind them. CRITICAL: every floating modal must be portaled to document.body so this shared z is compared in ONE root stacking context (an inline panel cannot beat siblings outside its own context no matter its z).
*/
-let topZ = 4000;
+let topZ = 10100;
/** Claim the front of the shared floating-window stack. Monotonic, session-length. */
export function nextFloatingZ(): number {