diff --git a/packages/dashboard/app/components/FloatingWindow.css b/packages/dashboard/app/components/FloatingWindow.css index f2087fcf7e..a956cbd28e 100644 --- a/packages/dashboard/app/components/FloatingWindow.css +++ b/packages/dashboard/app/components/FloatingWindow.css @@ -210,12 +210,16 @@ On mobile/narrow app viewports, opening Quick Chat should present the full Chat /* FNXC:FloatingWindow 2026-06-22-20:45: Body is a flex host that lets its single child stretch to the full panel width/height (min-width/min-height:0 so a wide child cannot collapse the flex line, and the child's own overflow can engage). + +FNXC:FloatingWindow 2026-07-15-00:00: +FN-8015 / issue #2140 requires every shared FloatingWindow caller to keep its hosted vertical scrollbar clear of the right-edge and right-corner resize hot zones. Reserve the corner-width gutter on the body rather than specializing task detail: this applies equally when the body itself scrolls and when a headerless caller delegates scrolling to its full-size child. The panel border and its right handles remain available for desktop resizing, while the scrollbar is always inboard of the east, north-east, and south-east hit targets. */ .floating-window__body { display: flex; flex: 1; min-width: 0; min-height: 0; + margin-inline-end: var(--space-lg); overflow: auto; } diff --git a/packages/dashboard/app/components/__tests__/FloatingWindow.test.tsx b/packages/dashboard/app/components/__tests__/FloatingWindow.test.tsx index 28efc479eb..0c9f8336c8 100644 --- a/packages/dashboard/app/components/__tests__/FloatingWindow.test.tsx +++ b/packages/dashboard/app/components/__tests__/FloatingWindow.test.tsx @@ -33,6 +33,11 @@ function cssRuleContaining(css: string, selector: string, declaration: string): return ""; } +function cssRulesForClass(css: string, className: string): string[] { + const escaped = className.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + return [...css.matchAll(new RegExp(`\\.${escaped}[^{}]*\\{[^}]*\\}`, "g"))].map((match) => match[0]); +} + /* FNXC:FloatingWindow 2026-06-22-20:45: Contract tests for the reusable non-blocking floating window: @@ -76,6 +81,91 @@ describe("FloatingWindow", () => { } }); + it("keeps every shared floating-window scrollbar inboard of the right resize hot zones", () => { + const bodyRule = floatingWindowCss.match(/(?:^|\n)\.floating-window__body\s*\{[^}]*\}/)?.[0] ?? ""; + + // The global scrollbar is 8px wide; the shared body reserves the 12px corner-handle gutter. + expect(stylesCss).toContain("*::-webkit-scrollbar {"); + expect(stylesCss).toContain("width: 8px;"); + expect(bodyRule).toContain("overflow: auto;"); + expect(bodyRule).toContain("margin-inline-end: var(--space-lg);"); + expect(cssRuleFor(floatingWindowCss, ".floating-window__resize-handle--e")).toContain("right: 0;"); + 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. + for (const callerClass of [ + "floating-window--task-detail", + "floating-window--automation", + "floating-window--mission-interview", + "floating-window--pr-create", + "floating-window--file-browser", + "floating-window--workflow-editor", + "artifacts-gallery-window", + ]) { + const rules = cssRulesForClass(allAppCss, 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")); + + expect(rightHandleRules.some((rule) => /(?:right|width)\s*:/.test(rule)), callerClass).toBe(false); + expect(bodyRules.some((rule) => /margin-inline-end\s*:/.test(rule)), callerClass).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;"); + }); + + it("keeps task-detail long content clear of right handles while preserving short-content right-edge resize", () => { + const longContent = Array.from({ length: 40 }, (_, index) =>
Scrollable task detail {index}
); + const { unmount } = render( +