FN-8474: remove tablet task popup gutter

Remove desktop resize clearance from tablet task-detail popups.

- Remove the tablet-only right body gutter and suppress resize handles.
- Preserve desktop resize clearance and mobile sheet behavior.
- Cover empty and populated tablet task popup contracts.

Files changed:
 .changeset/fn-8474-tablet-task-popup-gutter.md     |  7 +++
 packages/dashboard/app/components/FloatingWindow.css    | 14 ++++++
 packages/dashboard/app/components/__tests__/FloatingWindow.test.tsx   | 57 +++++++++++++++++++++-
 3 files changed, 77 insertions(+), 1 deletion(-)

Fusion-Task-Id: FN-8474

Fusion-Task-Lineage: 975478fa-c0da-4490-8b47-b09e02c6ff5e

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-22 00:09:30 -07:00
parent e68c48dabd
commit 82b8b0251e
3 changed files with 77 additions and 1 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Remove excess right padding from task popups on tablets.
category: fix
dev: Tablet task popups no longer reserve desktop resize-handle scrollbar clearance.

View File

@@ -106,6 +106,20 @@ Headerless task pop-outs still need a visible drag affordance. The embedded task
overflow: hidden; overflow: hidden;
} }
/*
FNXC:FloatingWindow 2026-07-21-00:00:
Tablet task-detail popups prioritize symmetric content insets over desktop resize affordances. Their body removes the desktop scrollbar-clearance gutter only while all resize handles are suppressed; desktop retains the clearance and resize-safe handles, and the delegated task header remains draggable.
*/
@media (min-width: 769px) and (max-width: 1024px) {
.floating-window--task-detail .floating-window__body {
margin-inline-end: 0;
}
.floating-window--task-detail .floating-window__resize-handle {
display: none;
}
}
/* /*
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.

View File

@@ -70,6 +70,20 @@ function stripAtMediaBlocks(css: string): string {
return out; return out;
} }
function mediaBlockFor(css: string, query: string): string {
const start = css.indexOf(`@media ${query}`);
if (start === -1) return "";
const open = css.indexOf("{", start);
let depth = 1;
let index = open + 1;
while (index < css.length && depth > 0) {
if (css[index] === "{") depth++;
else if (css[index] === "}") depth--;
index++;
}
return css.slice(open + 1, index - 1);
}
function setSheetViewport(isSheetWidth: boolean): void { function setSheetViewport(isSheetWidth: boolean): void {
vi.stubGlobal("matchMedia", vi.fn((query: string) => ({ vi.stubGlobal("matchMedia", vi.fn((query: string) => ({
matches: query === "(max-width: 768px)" ? isSheetWidth : query === "(max-height: 480px)", matches: query === "(max-width: 768px)" ? isSheetWidth : query === "(max-height: 480px)",
@@ -182,6 +196,47 @@ describe("FloatingWindow", () => {
expect(cssRuleFor(floatingWindowCss, ".floating-window--chat.floating-window--headerless .floating-window__body")).toContain("overflow: hidden;"); expect(cssRuleFor(floatingWindowCss, ".floating-window--chat.floating-window--headerless .floating-window__body")).toContain("overflow: hidden;");
}); });
it("removes only tablet task-detail resize clearance and handles for empty and populated popups", () => {
const tabletBlock = mediaBlockFor(floatingWindowCss, "(min-width: 769px) and (max-width: 1024px)");
const mobileBlock = mediaBlockFor(floatingWindowCss, "(max-width: 768px)");
const desktopCss = stripAtMediaBlocks(floatingWindowCss);
expect(cssRuleFor(tabletBlock, ".floating-window--task-detail .floating-window__body")).toContain("margin-inline-end: 0;");
expect(cssRuleFor(tabletBlock, ".floating-window--task-detail .floating-window__resize-handle")).toContain("display: none;");
expect(desktopCss.match(/(?:^|\n)\.floating-window__body\s*\{[^}]*\}/)?.[0]).toContain("margin-inline-end: var(--space-lg);");
expect(cssRulesForClass(desktopCss, "floating-window--task-detail").some((rule) => /margin-inline-end|display:\s*none/.test(rule))).toBe(false);
// Mobile remains a full-screen sheet with its independent no-gutter/no-handle contract.
expect(cssRuleFor(mobileBlock, ".floating-window--task-detail")).toContain("width: 100vw !important;");
expect(cssRuleFor(mobileBlock, ".floating-window--task-detail")).toContain("border-radius: 0;");
expect(cssRuleFor(mobileBlock, ".floating-window--task-detail .floating-window__body")).toContain("margin-inline-end: 0;");
expect(cssRuleFor(mobileBlock, ".floating-window--task-detail .floating-window__resize-handle")).toContain("display: none;");
expect(mobileBlock).toContain(".floating-window--task-detail .task-detail-content--embedded > .modal-header");
expect(mobileBlock).toContain("cursor: default;");
expect(mobileBlock).toContain("touch-action: auto;");
Object.defineProperty(window, "innerWidth", { configurable: true, value: 834 });
Object.defineProperty(window, "innerHeight", { configurable: true, value: 1112 });
setSheetViewport(false);
render(
<>
<FloatingWindow windowKey="tablet-empty" title="Empty task" onClose={() => {}} className="floating-window--task-detail">
<div aria-label="empty task detail" />
</FloatingWindow>
<FloatingWindow windowKey="tablet-populated" title="Populated task" onClose={() => {}} className="floating-window--task-detail">
<div aria-label="populated task detail">{Array.from({ length: 40 }, (_, index) => <p key={index}>Scrollable detail {index}</p>)}</div>
</FloatingWindow>
</>,
);
for (const key of ["tablet-empty", "tablet-populated"]) {
const panel = screen.getByTestId(`floating-window-${key}`);
expect(panel).toHaveClass("floating-window--task-detail");
expect(Number.parseFloat(panel.style.width)).toBeLessThanOrEqual(834);
expect(screen.getByTestId(`floating-window-body-${key}`)).toHaveClass("floating-window__body");
}
});
it("keeps task-detail long content clear of right handles while preserving short-content right-edge resize", () => { it("keeps task-detail long content clear of right handles while preserving short-content right-edge resize", () => {
const longContent = Array.from({ length: 40 }, (_, index) => <p key={index}>Scrollable task detail {index}</p>); const longContent = Array.from({ length: 40 }, (_, index) => <p key={index}>Scrollable task detail {index}</p>);
const { unmount } = render( const { unmount } = render(
@@ -950,6 +1005,6 @@ describe("FloatingWindow", () => {
expect(mobileBlock).toContain("width: 100vw !important;"); expect(mobileBlock).toContain("width: 100vw !important;");
expect(mobileBlock).toContain("height: 100dvh !important;"); expect(mobileBlock).toContain("height: 100dvh !important;");
expect(mobileBlock).toContain(".floating-window--chat .floating-window__resize-handle"); expect(mobileBlock).toContain(".floating-window--chat .floating-window__resize-handle");
expect(floatingWindowCss).not.toMatch(/@media\s*\(min-width:\s*769px\)[\s\S]*\.floating-window--chat[\s\S]*100dvh/); expect(mediaBlockFor(floatingWindowCss, "(min-width: 769px) and (max-width: 1024px)")).not.toContain(".floating-window--chat");
}); });
}); });