diff --git a/.changeset/tablet-floating-window-right-inset-and-drag.md b/.changeset/tablet-floating-window-right-inset-and-drag.md new file mode 100644 index 0000000000..26ac25c4f5 --- /dev/null +++ b/.changeset/tablet-floating-window-right-inset-and-drag.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Fix uneven right padding on tablet task/terminal popups; drag the terminal from anywhere in its toolbar. +category: fix +dev: Tablet-mode FloatingWindows (`.floating-window--tablet`) zero the FN-8015 scrollbar gutter; GitHub-import detail compensates locally. Terminal tab-strip empty space now bubbles to the `.terminal-header` drag handle (`touch-action: none` on the tablet floating header supersedes the FN-8633 pan-x contract). diff --git a/packages/dashboard/app/components/FloatingWindow.css b/packages/dashboard/app/components/FloatingWindow.css index d3a69ad1d5..60d5f60766 100644 --- a/packages/dashboard/app/components/FloatingWindow.css +++ b/packages/dashboard/app/components/FloatingWindow.css @@ -539,6 +539,22 @@ FN-8015 / issue #2140 requires every shared FloatingWindow caller to keep its ho overflow: auto; } +/* +FNXC:ModalTouchGeometry 2026-08-01-03:48: +Tablet windows drop FN-8015's inline-end scrollbar gutter. On tablet the gutter reads as a +too-big, uneven right border inside Task Detail popups and the floating terminal (the inner +surfaces already carry their own symmetric padding), and the mouse conflict FN-8015 solves — +grabbing a hosted scrollbar thumb versus the east resize hot zone — does not apply to touch +input, which never grabs scrollbar thumbs. This keys on tablet MODE (`.floating-window--tablet`, +set from `useViewportMode()`), not `--touch-geometry`, so a non-touch window at tablet widths +gets the same symmetric inset the operator sees on an iPad. Desktop keeps the FN-8015 gutter. +Callers that relied on the gutter as their right inset must compensate under the same class +(see `.github-import-detail-panel`). +*/ +.floating-window--tablet .floating-window__body { + margin-inline-end: 0; +} + .floating-window__body > * { flex: 1; min-width: 0; diff --git a/packages/dashboard/app/components/FloatingWindow.tsx b/packages/dashboard/app/components/FloatingWindow.tsx index 2de1a1bc56..1431181356 100644 --- a/packages/dashboard/app/components/FloatingWindow.tsx +++ b/packages/dashboard/app/components/FloatingWindow.tsx @@ -232,6 +232,16 @@ export function FloatingWindow({ mouse geometry; a known touch tablet at 768px is the one surface that receives enlarged targets. */ const hasTabletTouchGeometry = isTabletTouchViewport(viewportMode); + /* + FNXC:ModalTouchGeometry 2026-08-01-03:48: + Tablet MODE (touch or not) is a distinct styling surface from touch geometry: a 900px + non-touch window classifies tablet without `--touch-geometry`, yet operators still see the + FN-8015 scrollbar gutter as an uneven right inset there (third recurrence of the Task Detail + right-padding bug — FN-8630/FN-8634 fixed only the `.modal-overlay` shells, while every + tablet task popup and floating terminal renders through THIS host). Expose the mode as a + class so FloatingWindow.css can zero the gutter for all tablet windows. + */ + const isTabletMode = viewportMode === "tablet"; const initialGeometry = useRef<{ size: FloatingWindowSize; position: FloatingWindowPosition } | null>(null); /* FNXC:ModalGeometryPersistence 2026-07-16-00:40: @@ -638,7 +648,7 @@ export function FloatingWindow({ >
Scrollable task detail {index}
); const { unmount } = render( diff --git a/packages/dashboard/app/components/__tests__/TerminalModal.test.tsx b/packages/dashboard/app/components/__tests__/TerminalModal.test.tsx index 42a85a7901..d9a9a6b87f 100644 --- a/packages/dashboard/app/components/__tests__/TerminalModal.test.tsx +++ b/packages/dashboard/app/components/__tests__/TerminalModal.test.tsx @@ -1458,6 +1458,22 @@ describe("TerminalModal", () => { fireEvent.click(screen.getAllByRole("tab")[1]); expect(setActiveTab).toHaveBeenCalledWith("tab-2"); expect(panel.setPointerCapture).toHaveBeenCalledTimes(1); + + /* + FNXC:TerminalModalControls 2026-08-01-03:48: + Empty strip space behind the tabs is a drag surface: a press that starts on the + `.terminal-tabs` container itself (not inside a `.terminal-tab`) must bubble to the + FloatingWindow `.terminal-header` delegated drag handle and move the window. + */ + const tabStrip = screen.getByTestId("terminal-tabs"); + const preDragLeft = panel.style.left; + fireEvent.pointerDown(tabStrip, { pointerId: 65, pointerType: "touch", clientX: 300, clientY: 40 }); + fireEvent.pointerMove(panel, { pointerId: 65, pointerType: "touch", clientX: 380, clientY: 90 }); + fireEvent.pointerUp(panel, { pointerId: 65, pointerType: "touch", clientX: 380, clientY: 90 }); + await waitFor(() => { + expect(panel.setPointerCapture).toHaveBeenCalledTimes(2); + expect(panel.style.left).not.toBe(preDragLeft); + }); unmount(); } finally { Object.defineProperty(window, "innerWidth", { configurable: true, value: previousInnerWidth }); @@ -1467,24 +1483,31 @@ describe("TerminalModal", () => { } }); - it("keeps the tablet drag grip isolated from the horizontally pannable tab strip", () => { + /* + FNXC:TerminalModalControls 2026-08-01-03:48: + Supersedes the FN-8633 grip-isolation contract: the operator wants the floating terminal + draggable from the empty strip space behind the tabs and anywhere in the top toolbar. The + tablet floating header AND tab strip hand the whole touch gesture to FloatingWindow's pointer + drag (`touch-action: none`); native strip panning is moot because an overflowing strip is + replaced by the mobile-tabs dropdown. The grip remains as the visible movability affordance. + */ + it("makes the whole tablet floating header a touch drag surface while keeping the grip affordance", () => { const gripSelector = ".modal.terminal-modal.terminal-modal--tablet.terminal-modal--floating .terminal-header__drag-grip"; - const tabsSelector = ".modal.terminal-modal.terminal-modal--tablet.terminal-modal--floating .terminal-tabs"; const gripSelectorIndex = terminalModalCss.indexOf(gripSelector); const gripRuleEnd = terminalModalCss.indexOf("}", gripSelectorIndex); const gripRule = terminalModalCss.slice(gripSelectorIndex, gripRuleEnd); - const tabsSelectorIndex = terminalModalCss.indexOf(tabsSelector); - const tabsRuleEnd = terminalModalCss.indexOf("}", tabsSelectorIndex); - const tabsRule = terminalModalCss.slice(tabsSelectorIndex, tabsRuleEnd); + const headerTouchSelector = ".modal.terminal-modal.terminal-modal--tablet.terminal-modal--floating .terminal-header,\n.modal.terminal-modal.terminal-modal--tablet.terminal-modal--floating .terminal-tabs"; + const headerTouchIndex = terminalModalCss.indexOf(headerTouchSelector); + const headerTouchRule = terminalModalCss.slice(headerTouchIndex, terminalModalCss.indexOf("}", headerTouchIndex)); expect(gripSelectorIndex).toBeGreaterThan(-1); expect(gripRule).toContain("min-block-size: var(--modal-resize-touch-target);"); expect(gripRule).toContain("min-inline-size: var(--modal-resize-touch-target);"); expect(gripRule).toContain("touch-action: none;"); - expect(tabsSelectorIndex).toBeGreaterThan(-1); - expect(tabsRule).toContain("touch-action: pan-x;"); - expect(terminalModalCss).not.toContain(".modal.terminal-modal.terminal-modal--tablet.terminal-modal--floating {\n touch-action: none;"); - expect(terminalModalCss).not.toContain(".modal.terminal-modal.terminal-modal--tablet.terminal-modal--floating .terminal-header {\n touch-action: none;"); + expect(headerTouchIndex).toBeGreaterThan(-1); + expect(headerTouchRule).toContain("touch-action: none;"); + // Grab affordance on the floating header/strip; real tabs keep the base pointer cursor. + expect(terminalModalCss).toContain(".modal.terminal-modal.terminal-modal--floating .terminal-header,\n.modal.terminal-modal.terminal-modal--floating .terminal-tabs {\n cursor: grab;"); expect(terminalModalCss.slice(0, gripSelectorIndex)).not.toContain("@media (min-width: 769px)"); expect(loadAllAppCss()).toContain(gripSelector); });