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 {