diff --git a/.changeset/fn-mobile-task-detail-left-shift.md b/.changeset/fn-mobile-task-detail-left-shift.md new file mode 100644 index 0000000000..4f32196df1 --- /dev/null +++ b/.changeset/fn-mobile-task-detail-left-shift.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Fix the mobile task detail panel being shifted left with a dead gutter on the right. +category: fix +dev: The full-screen mobile task-detail sheet hides all resize handles, so FN-8015's `margin-inline-end: var(--space-lg)` scrollbar/resize-hot-zone gutter on the shared `.floating-window__body` only added dead space on the right. Zeroed it for `.floating-window--task-detail` inside the mobile breakpoint; desktop resize-handle clearance is untouched. diff --git a/packages/dashboard/app/components/FloatingWindow.css b/packages/dashboard/app/components/FloatingWindow.css index 8113497def..4416c193e9 100644 --- a/packages/dashboard/app/components/FloatingWindow.css +++ b/packages/dashboard/app/components/FloatingWindow.css @@ -192,6 +192,21 @@ On mobile/narrow app viewports, opening Quick Chat should present the full Chat display: none; } + /* + FNXC:MobileTaskPopups 2026-07-17-08:20: + FN-8015 reserves `margin-inline-end: var(--space-lg)` on the shared + `.floating-window__body` so a hosted scrollbar clears the desktop + east/north-east/south-east resize hot zones. The mobile task-detail sheet is + full-screen and hides every resize handle (above), so that gutter protects + nothing and instead shifts the whole panel left — leaving an uneven right + inset on every row. Zero it here so `.detail-body`'s own symmetric padding + defines both insets equally. Desktop resize-handle clearance is untouched + (this rule is mobile-only). + */ + .floating-window--task-detail .floating-window__body { + margin-inline-end: 0; + } + .floating-window--task-detail .task-detail-content--embedded, .floating-window--task-detail .task-detail-content--embedded > .modal-header { border-radius: 0; diff --git a/packages/dashboard/app/components/__tests__/FloatingWindow.test.tsx b/packages/dashboard/app/components/__tests__/FloatingWindow.test.tsx index 3c859685f3..6dde8853f2 100644 --- a/packages/dashboard/app/components/__tests__/FloatingWindow.test.tsx +++ b/packages/dashboard/app/components/__tests__/FloatingWindow.test.tsx @@ -38,6 +38,38 @@ function cssRulesForClass(css: string, className: string): string[] { return [...css.matchAll(new RegExp(`\\.${escaped}[^{}]*\\{[^}]*\\}`, "g"))].map((match) => match[0]); } +/* +FNXC:FloatingWindow 2026-07-17-08:20: +The FN-8015 desktop resize-hot-zone invariant only governs desktop widths. The +mobile full-screen sheet variants hide every resize handle, so removing the +inherited body gutter there is legitimate (and required — see the mobile +task-detail left-shift fix). Strip `@media` blocks with balanced-brace matching +before scanning so the desktop invariant ignores mobile-only overrides. +*/ +function stripAtMediaBlocks(css: string): string { + let out = ""; + let i = 0; + while (i < css.length) { + const at = css.indexOf("@media", i); + if (at === -1) { + out += css.slice(i); + break; + } + out += css.slice(i, at); + const open = css.indexOf("{", at); + if (open === -1) break; + let depth = 1; + let j = open + 1; + while (j < css.length && depth > 0) { + if (css[j] === "{") depth++; + else if (css[j] === "}") depth--; + j++; + } + i = j; + } + return out; +} + function setSheetViewport(isSheetWidth: boolean): void { vi.stubGlobal("matchMedia", vi.fn((query: string) => ({ matches: query === "(max-width: 768px)" ? isSheetWidth : query === "(max-height: 480px)", @@ -107,7 +139,10 @@ describe("FloatingWindow", () => { expect(cssRuleFor(floatingWindowCss, ".floating-window__resize-handle--ne")).toContain("right: 0;"); expect(cssRuleFor(floatingWindowCss, ".floating-window__resize-handle--se")).toContain("right: 0;"); - // No shared caller may move a right handle back into the reserved scrollbar gutter. + // 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 + // sheet overrides (inside @media) are legitimate and excluded from this scan. + const desktopAppCss = stripAtMediaBlocks(allAppCss); for (const callerClass of [ "floating-window--task-detail", "floating-window--automation", @@ -117,7 +152,7 @@ describe("FloatingWindow", () => { "floating-window--workflow-editor", "artifacts-gallery-window", ]) { - const rules = cssRulesForClass(allAppCss, callerClass); + const rules = cssRulesForClass(desktopAppCss, callerClass); const rightHandleRules = rules.filter((rule) => /floating-window__resize-handle(?:--(?:e|ne|se))?/.test(rule)); const bodyRules = rules.filter((rule) => rule.includes("floating-window__body")); @@ -125,6 +160,23 @@ describe("FloatingWindow", () => { expect(bodyRules.some((rule) => /margin-inline-end\s*:/.test(rule)), callerClass).toBe(false); } + /* + FNXC:MobileTaskPopups 2026-07-17-08:20: + Regression guard for the mobile task-detail left-shift fix: the full-screen + task-detail sheet hides all resize handles, so FN-8015's inherited + `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( + allAppCss, + ".floating-window--task-detail .floating-window__body", + "margin-inline-end", + ); + expect(mobileTaskDetailBody).toContain("margin-inline-end: 0;"); + expect(cssRulesForClass(desktopAppCss, "floating-window--task-detail").some((rule) => rule.includes("floating-window__body"))).toBe(false); + // Headerless and chat variants replace only body overflow; the inherited gutter remains intact for their inner scrollers. expect(cssRuleFor(floatingWindowCss, ".floating-window--headerless .floating-window__body")).toContain("overflow: hidden;"); expect(cssRuleFor(floatingWindowCss, ".floating-window--chat.floating-window--headerless .floating-window__body")).toContain("overflow: hidden;");