FN-8766: align task detail popup inset
Align the desktop Task Detail popup header and content edges without losing resize access. - Remove the Task Detail desktop body gutter while preserving shared scrollbar clearance. - Move Task Detail east resize targets outside the painted shell and retain mobile clipping. - Add geometry regression coverage and a patch changeset. Files changed: .changeset/fn-8766-task-detail-popup-spacing.md | 7 +++ .../dashboard/app/components/FloatingWindow.css | 30 +++++++++++ .../components/__tests__/FloatingWindow.test.tsx | 32 +++++++----- ...etailModal.responsive-and-dependencies.test.tsx | 58 ++++++++++++++++++++++ 4 files changed, 115 insertions(+), 12 deletions(-) Fusion-Task-Id: FN-8766 Fusion-Task-Lineage: b4d23a5e-51d0-45cb-8cbb-4fefb81fa09f Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-8766-task-detail-popup-spacing.md
Normal file
7
.changeset/fn-8766-task-detail-popup-spacing.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Align the floating Task Detail popup close control with its header edge.
|
||||||
|
category: fix
|
||||||
|
dev: Moves only Task Detail desktop resize targets outboard while retaining shared scrollbar clearance.
|
||||||
@@ -261,6 +261,35 @@ Phone sheets are strictly below 768px. At exactly 768px this query must not matc
|
|||||||
FNXC:ChatModal 2026-06-22-14:49:
|
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.
|
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.
|
||||||
*/
|
*/
|
||||||
|
/*
|
||||||
|
FNXC:TaskDetailLayout 2026-08-03-19:36:
|
||||||
|
FN-8766 keeps the desktop Task Detail popup's visible header/content edge symmetric while
|
||||||
|
preserving the shared body's scrollbar clearance for every other FloatingWindow consumer. This
|
||||||
|
headerless host owns its close button, so remove only its visible body gutter and move its desktop
|
||||||
|
east resize hit areas outside the painted shell; the scrollbar remains fully usable and the close
|
||||||
|
control does not sit behind a resize target. Tablet retains its existing touch geometry and phones
|
||||||
|
remain full-screen sheets below.
|
||||||
|
*/
|
||||||
|
.floating-window--task-detail:not(.floating-window--tablet-viewport) {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-window--task-detail:not(.floating-window--tablet-viewport) .floating-window__body {
|
||||||
|
margin-inline-end: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-window--task-detail:not(.floating-window--tablet-viewport) .floating-window__resize-handle--e {
|
||||||
|
right: calc(var(--space-sm) * -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-window--task-detail:not(.floating-window--tablet-viewport) .floating-window__resize-handle--ne {
|
||||||
|
right: calc(var(--space-lg) * -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-window--task-detail:not(.floating-window--tablet-viewport) .floating-window__resize-handle--se {
|
||||||
|
right: calc(var(--space-lg) * -1);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
FNXC:ModalTouchGeometry 2026-07-26-13:25:
|
FNXC:ModalTouchGeometry 2026-07-26-13:25:
|
||||||
FN-8606 migrated core and workflow dialogs share FloatingWindow's pointer contract. Their
|
FN-8606 migrated core and workflow dialogs share FloatingWindow's pointer contract. Their
|
||||||
@@ -452,6 +481,7 @@ all floating affordances so persisted desktop geometry cannot imply a draggable
|
|||||||
*/
|
*/
|
||||||
.floating-window--task-detail {
|
.floating-window--task-detail {
|
||||||
inset: 0 !important;
|
inset: 0 !important;
|
||||||
|
overflow: hidden !important;
|
||||||
width: 100vw !important;
|
width: 100vw !important;
|
||||||
height: 100dvh !important;
|
height: 100dvh !important;
|
||||||
min-width: 0 !important;
|
min-width: 0 !important;
|
||||||
|
|||||||
@@ -161,12 +161,14 @@ describe("FloatingWindow", () => {
|
|||||||
expect(cssRuleContaining(floatingWindowCss, ".floating-window__resize-handle--ne", "right: 0;")).toContain("right: 0;");
|
expect(cssRuleContaining(floatingWindowCss, ".floating-window__resize-handle--ne", "right: 0;")).toContain("right: 0;");
|
||||||
expect(cssRuleContaining(floatingWindowCss, ".floating-window__resize-handle--se", "right: 0;")).toContain("right: 0;");
|
expect(cssRuleContaining(floatingWindowCss, ".floating-window__resize-handle--se", "right: 0;")).toContain("right: 0;");
|
||||||
|
|
||||||
// No shared caller may move a right handle back into the reserved scrollbar
|
/*
|
||||||
// gutter, nor override the body gutter, AT DESKTOP WIDTHS. Mobile full-screen
|
FNXC:TaskDetailLayout 2026-08-03-19:36:
|
||||||
// sheet overrides (inside @media) are legitimate and excluded from this scan.
|
FN-8766 is the sole desktop exception: its header-owned close control requires a symmetric
|
||||||
|
painted edge, so its gutter is removed only while its east resize targets move outboard.
|
||||||
|
Every unrelated consumer keeps the shared scrollbar-clearance geometry.
|
||||||
|
*/
|
||||||
const desktopAppCss = stripAtMediaBlocks(allAppCss);
|
const desktopAppCss = stripAtMediaBlocks(allAppCss);
|
||||||
for (const callerClass of [
|
for (const callerClass of [
|
||||||
"floating-window--task-detail",
|
|
||||||
"floating-window--automation",
|
"floating-window--automation",
|
||||||
"floating-window--mission-interview",
|
"floating-window--mission-interview",
|
||||||
"floating-window--pr-create",
|
"floating-window--pr-create",
|
||||||
@@ -182,14 +184,20 @@ describe("FloatingWindow", () => {
|
|||||||
expect(bodyRules.some((rule) => /margin-inline-end\s*:/.test(rule)), callerClass).toBe(false);
|
expect(bodyRules.some((rule) => /margin-inline-end\s*:/.test(rule)), callerClass).toBe(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const desktopTaskBody = cssRuleContaining(
|
||||||
|
desktopAppCss,
|
||||||
|
".floating-window--task-detail:not(.floating-window--tablet-viewport) .floating-window__body",
|
||||||
|
"margin-inline-end",
|
||||||
|
);
|
||||||
|
expect(desktopTaskBody).toContain("margin-inline-end: 0;");
|
||||||
|
expect(cssRuleContaining(desktopAppCss, ".floating-window--task-detail:not(.floating-window--tablet-viewport) .floating-window__resize-handle--e", "right")).toContain("right: calc(var(--space-sm) * -1);");
|
||||||
|
expect(cssRuleContaining(desktopAppCss, ".floating-window--task-detail:not(.floating-window--tablet-viewport) .floating-window__resize-handle--ne", "right")).toContain("right: calc(var(--space-lg) * -1);");
|
||||||
|
expect(cssRuleContaining(desktopAppCss, ".floating-window--task-detail:not(.floating-window--tablet-viewport) .floating-window__resize-handle--se", "right")).toContain("right: calc(var(--space-lg) * -1);");
|
||||||
|
|
||||||
/*
|
/*
|
||||||
FNXC:MobileTaskPopups 2026-07-17-08:20:
|
FNXC:MobileTaskPopups 2026-08-03-19:36:
|
||||||
Regression guard for the mobile task-detail left-shift fix: the full-screen
|
The full-screen task-detail sheet hides every resize handle, so the inherited gutter must
|
||||||
task-detail sheet hides all resize handles, so FN-8015's inherited
|
remain zero and the desktop outboard-target host must resume clipping for sheet geometry.
|
||||||
`margin-inline-end: var(--space-lg)` body gutter only added dead space on the
|
|
||||||
right and shifted the whole panel left. The mobile breakpoint must zero it so
|
|
||||||
`.detail-body`'s own padding defines both insets equally. This is the sole
|
|
||||||
legitimate body-gutter override and lives only inside the mobile @media block.
|
|
||||||
*/
|
*/
|
||||||
const mobileTaskDetailBody = cssRuleContaining(
|
const mobileTaskDetailBody = cssRuleContaining(
|
||||||
allAppCss,
|
allAppCss,
|
||||||
@@ -197,7 +205,7 @@ describe("FloatingWindow", () => {
|
|||||||
"margin-inline-end",
|
"margin-inline-end",
|
||||||
);
|
);
|
||||||
expect(mobileTaskDetailBody).toContain("margin-inline-end: 0;");
|
expect(mobileTaskDetailBody).toContain("margin-inline-end: 0;");
|
||||||
expect(cssRulesForClass(desktopAppCss, "floating-window--task-detail").some((rule) => rule.includes("floating-window__body"))).toBe(false);
|
expect(cssRuleContaining(allAppCss, ".floating-window--task-detail", "overflow: hidden !important;")).toContain("overflow: hidden !important;");
|
||||||
|
|
||||||
/*
|
/*
|
||||||
FNXC:ModalTouchGeometry 2026-08-01-03:48:
|
FNXC:ModalTouchGeometry 2026-08-01-03:48:
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
setupTaskDetailModalHooks,
|
setupTaskDetailModalHooks,
|
||||||
} from "./TaskDetailModal.test-helpers";
|
} from "./TaskDetailModal.test-helpers";
|
||||||
import { TaskDetailModal, TaskDetailContent } from "../TaskDetailModal";
|
import { TaskDetailModal, TaskDetailContent } from "../TaskDetailModal";
|
||||||
|
import { FloatingWindow } from "../FloatingWindow";
|
||||||
import {
|
import {
|
||||||
assertModalGeometryRecoveryAndSheetContracts,
|
assertModalGeometryRecoveryAndSheetContracts,
|
||||||
assertRenderedModalTouchGeometry,
|
assertRenderedModalTouchGeometry,
|
||||||
@@ -856,6 +857,63 @@ describe("TaskDetailModal", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps the floating task header symmetric without sacrificing its resize targets", () => {
|
||||||
|
const floatingCss = readFileSync(resolve(__dirname, "../FloatingWindow.css"), "utf8");
|
||||||
|
const desktopTaskSelector = ".floating-window--task-detail:not(.floating-window--tablet-viewport)";
|
||||||
|
const taskPopupBody = getExactCssRuleBlock(floatingCss, `${desktopTaskSelector} .floating-window__body`);
|
||||||
|
const sharedBody = getExactCssRuleBlock(floatingCss, ".floating-window__body");
|
||||||
|
const header = getExactCssRuleBlock(readDashboardStylesSource(), ".modal-header");
|
||||||
|
const eastResize = getExactCssRuleBlock(floatingCss, `${desktopTaskSelector} .floating-window__resize-handle--e`);
|
||||||
|
const northEastResize = getExactCssRuleBlock(floatingCss, `${desktopTaskSelector} .floating-window__resize-handle--ne`);
|
||||||
|
const southEastResize = getExactCssRuleBlock(floatingCss, `${desktopTaskSelector} .floating-window__resize-handle--se`);
|
||||||
|
const onRequestClose = vi.fn();
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:TaskDetailLayout 2026-08-03-19:36:
|
||||||
|
The shared body reserves desktop scrollbar clearance. Task Detail moves only its resize
|
||||||
|
hit areas outboard, so the embedded header retains matching tokenized edges.
|
||||||
|
*/
|
||||||
|
expect(sharedBody).toContain("margin-inline-end: var(--space-lg);");
|
||||||
|
expect(taskPopupBody).toContain("margin-inline-end: 0;");
|
||||||
|
expect(header).toContain("padding: var(--modal-padding);");
|
||||||
|
expect(eastResize).toContain("right: calc(var(--space-sm) * -1);");
|
||||||
|
expect(northEastResize).toContain("right: calc(var(--space-lg) * -1);");
|
||||||
|
expect(southEastResize).toContain("right: calc(var(--space-lg) * -1);");
|
||||||
|
|
||||||
|
const { baseElement, unmount } = render(
|
||||||
|
<FloatingWindow
|
||||||
|
windowKey="task-detail-inset"
|
||||||
|
title="FN-8766"
|
||||||
|
onClose={onRequestClose}
|
||||||
|
hideHeader
|
||||||
|
dragHandleSelector=".task-detail-content--embedded > .modal-header"
|
||||||
|
className="floating-window--task-detail"
|
||||||
|
layer="task-detail"
|
||||||
|
>
|
||||||
|
<TaskDetailContent
|
||||||
|
task={makeTask({ id: "FN-8766", title: "A task title long enough to exercise header alignment" })}
|
||||||
|
onMoveTask={noopMove}
|
||||||
|
onDeleteTask={noopDelete}
|
||||||
|
onMergeTask={noopMerge}
|
||||||
|
onOpenDetail={noopOpenDetail}
|
||||||
|
addToast={noop}
|
||||||
|
embedded
|
||||||
|
onRequestClose={onRequestClose}
|
||||||
|
/>
|
||||||
|
</FloatingWindow>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const popup = baseElement.querySelector("[data-testid='floating-window-task-detail-inset']");
|
||||||
|
const close = screen.getByRole("button", { name: "Close" });
|
||||||
|
expect(popup).toHaveClass("floating-window--task-detail");
|
||||||
|
expect(popup?.querySelector(".task-detail-content--embedded > .modal-header")).toContainElement(close);
|
||||||
|
expect(close).toHaveClass("task-detail-floating-close");
|
||||||
|
expect(popup?.querySelectorAll(".floating-window__resize-handle")).toHaveLength(8);
|
||||||
|
fireEvent.click(close);
|
||||||
|
expect(onRequestClose).toHaveBeenCalledTimes(1);
|
||||||
|
unmount();
|
||||||
|
});
|
||||||
|
|
||||||
it("uses production FloatingWindow geometry for touch drag, eight-direction resize, recovery, and sheets", () => {
|
it("uses production FloatingWindow geometry for touch drag, eight-direction resize, recovery, and sheets", () => {
|
||||||
const mount = () => render(
|
const mount = () => render(
|
||||||
<TaskDetailModal
|
<TaskDetailModal
|
||||||
|
|||||||
Reference in New Issue
Block a user