fix: even out tablet popup right inset; drag terminal from whole toolbar
Third recurrence of the Task Detail right-padding bug (FN-8630/FN-8634): those fixes only covered the .modal-overlay shells, while every tablet task popup and floating terminal renders through FloatingWindow, whose shared body carries FN-8015's margin-inline-end scrollbar gutter — a 16px right border with a 0px left one. Tablet-mode windows (.floating-window--tablet, keyed on viewport MODE so non-touch tablet widths match too) now zero the gutter; touch never grabs scrollbar thumbs, so the desktop hot-zone conflict FN-8015 solves cannot occur. GitHub-import's detail panel, which used the gutter as its right inset, compensates locally. Desktop keeps the FN-8015 contract. Also per operator request: the floating terminal is draggable from the empty strip space behind the tabs and anywhere in the top toolbar, not only the FN-8633 grip. Tab presses keep stopPropagation (scoped to .terminal-tab) so they never start a window drag; the tablet floating header supersedes the pan-x contract with touch-action: none (an overflowing strip is replaced by the mobile-tabs dropdown, so no visible strip pans horizontally). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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).
|
||||
@@ -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;
|
||||
|
||||
@@ -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({
|
||||
>
|
||||
<div
|
||||
ref={panelRef}
|
||||
className={`floating-window${hideHeader ? " floating-window--headerless" : ""}${hasTabletTouchGeometry ? " floating-window--touch-geometry" : ""}${className ? ` ${className}` : ""}`}
|
||||
className={`floating-window${hideHeader ? " floating-window--headerless" : ""}${hasTabletTouchGeometry ? " floating-window--touch-geometry" : ""}${isTabletMode ? " floating-window--tablet" : ""}${className ? ` ${className}` : ""}`}
|
||||
style={panelStyle}
|
||||
data-testid={`floating-window-${windowKey}`}
|
||||
onPointerDownCapture={bringToFront}
|
||||
|
||||
@@ -327,6 +327,16 @@ Padding lives on the panel rather than the pane content so header, toast, and sc
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:GitHubImport 2026-08-01-03:48:
|
||||
Tablet windows zero the shared FN-8015 body gutter (`.floating-window--tablet .floating-window__body`
|
||||
in FloatingWindow.css) because it rendered as an uneven right border on tablet. This panel's right
|
||||
inset was supplied by that gutter, so restore it locally to keep the panel's insets symmetric.
|
||||
*/
|
||||
.floating-window--tablet .github-import-detail-panel {
|
||||
padding-inline-end: var(--space-lg);
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:GitHubImport 2026-07-15-23:25:
|
||||
Bottom action bar for the detail preview. Import/Close issue moved here from the pane header —
|
||||
|
||||
@@ -244,9 +244,12 @@ FN-7897: reserve space for the fixed ExecutorStatusBar footer when it is rendere
|
||||
/*
|
||||
FNXC:TerminalModalControls 2026-07-31-22:41:
|
||||
The tablet classifier, rather than the legacy 769px media query, owns this grip so a known
|
||||
768px touch tablet remains a floating window instead of receiving phone-sheet chrome. Only the
|
||||
reserved grip disables browser gesture handling: applying `touch-action: none` to the panel or
|
||||
header prevents native horizontal panning in the descendant overflowing tab strip.
|
||||
768px touch tablet remains a floating window instead of receiving phone-sheet chrome.
|
||||
|
||||
FNXC:TerminalModalControls 2026-08-01-03:48:
|
||||
The grip is no longer the sole drag surface — the whole tablet floating header (including empty
|
||||
tab-strip space) is `touch-action: none` and draggable below — but it stays as the always-visible
|
||||
affordance that signals the window is movable.
|
||||
*/
|
||||
.modal.terminal-modal.terminal-modal--tablet.terminal-modal--floating .terminal-header__drag-grip {
|
||||
position: relative;
|
||||
@@ -274,12 +277,19 @@ header prevents native horizontal panning in the descendant overflowing tab stri
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:TerminalModalControls 2026-07-31-22:41:
|
||||
The tab strip must retain native horizontal touch panning while the sibling grip owns window
|
||||
movement. This explicit contract prevents future tablet drag rules from suppressing tab scrolling.
|
||||
FNXC:TerminalModalControls 2026-08-01-03:48:
|
||||
Supersedes the 2026-07-31 pan-x contract: the operator wants the floating terminal draggable
|
||||
from the empty strip space behind the tabs and anywhere in the top toolbar, not only the grip.
|
||||
`touch-action: none` on the tablet floating header (and its strip) hands the whole touch gesture
|
||||
to FloatingWindow's pointer drag; without it the browser claims horizontal movement as a native
|
||||
pan and pointercancels the drag mid-gesture. Native strip panning is not lost: an overflowing
|
||||
strip is replaced by the `.terminal-mobile-tabs` dropdown (ResizeObserver in TerminalModal.tsx),
|
||||
so a visible strip never scrolls horizontally. Tab presses stay tab presses via the scoped
|
||||
stopPropagation in renderTerminalTabStrip.
|
||||
*/
|
||||
.modal.terminal-modal.terminal-modal--tablet.terminal-modal--floating .terminal-header,
|
||||
.modal.terminal-modal.terminal-modal--tablet.terminal-modal--floating .terminal-tabs {
|
||||
touch-action: pan-x;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -337,6 +347,17 @@ hit area; it is not a second terminal control.
|
||||
border-bottom-color: var(--in-progress);
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:TerminalModalControls 2026-08-01-03:48:
|
||||
The floating header (including empty tab-strip space) is a window drag surface at every pointer
|
||||
type; show the grab affordance there while real tab surfaces keep the base `.terminal-tab`
|
||||
pointer cursor above.
|
||||
*/
|
||||
.modal.terminal-modal.terminal-modal--floating .terminal-header,
|
||||
.modal.terminal-modal.terminal-modal--floating .terminal-tabs {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.terminal-tab-indicator {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
|
||||
@@ -2427,9 +2427,14 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG
|
||||
);
|
||||
|
||||
/*
|
||||
FNXC:TerminalModalControls 2026-07-31-22:19:
|
||||
The tab scroller owns tab presses and horizontal panning; floating-window drags begin only on
|
||||
the reserved grip so a tab interaction cannot leave a captured header drag behind.
|
||||
FNXC:TerminalModalControls 2026-08-01-03:48:
|
||||
The floating terminal must be movable by dragging the empty strip space behind the tabs and
|
||||
anywhere else in the top toolbar, not only the reserved grip (operator request following
|
||||
FN-8633). Only a press that starts inside a real tab surface (`.terminal-tab`, which includes
|
||||
the close and new-tab buttons) stays a tab interaction; empty strip space bubbles to the
|
||||
FloatingWindow `.terminal-header` delegated drag handle. Tab presses keep stopPropagation so
|
||||
they can never leave a captured header drag behind, and an overflowing strip is replaced by
|
||||
the `.terminal-mobile-tabs` dropdown, so no visible strip ever needs horizontal panning.
|
||||
*/
|
||||
const renderTerminalTabStrip = (measuring = false) => (
|
||||
<div
|
||||
@@ -2437,7 +2442,10 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG
|
||||
className={`terminal-tabs${measuring ? " terminal-tabs--measuring" : ""}`}
|
||||
data-testid={measuring ? "terminal-tabs-measuring" : "terminal-tabs"}
|
||||
aria-hidden={measuring || undefined}
|
||||
onPointerDown={measuring ? undefined : (event) => event.stopPropagation()}
|
||||
onPointerDown={measuring ? undefined : (event) => {
|
||||
const target = event.target as HTMLElement | null;
|
||||
if (target?.closest(".terminal-tab")) event.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{tabs.map((tab) => (
|
||||
<div
|
||||
|
||||
@@ -199,6 +199,28 @@ describe("FloatingWindow", () => {
|
||||
expect(mobileTaskDetailBody).toContain("margin-inline-end: 0;");
|
||||
expect(cssRulesForClass(desktopAppCss, "floating-window--task-detail").some((rule) => rule.includes("floating-window__body"))).toBe(false);
|
||||
|
||||
/*
|
||||
FNXC:ModalTouchGeometry 2026-08-01-03:48:
|
||||
Tablet contract: ALL tablet-mode floating windows zero the FN-8015 gutter (it read as a
|
||||
too-big uneven right border in Task Detail popups and the floating terminal — third
|
||||
recurrence of the right-padding bug because tablet renders through FloatingWindow, not
|
||||
`.modal-overlay`). Touch never grabs scrollbar thumbs, so the desktop hot-zone conflict
|
||||
does not apply. GitHubImport relied on the gutter as its right inset and must compensate
|
||||
under the same class.
|
||||
*/
|
||||
const tabletBody = cssRuleContaining(
|
||||
floatingWindowCss,
|
||||
".floating-window--tablet .floating-window__body",
|
||||
"margin-inline-end",
|
||||
);
|
||||
expect(tabletBody).toContain("margin-inline-end: 0;");
|
||||
const tabletGitHubImportPanel = cssRuleContaining(
|
||||
allAppCss,
|
||||
".floating-window--tablet .github-import-detail-panel",
|
||||
"padding-inline-end",
|
||||
);
|
||||
expect(tabletGitHubImportPanel).toContain("padding-inline-end: var(--space-lg);");
|
||||
|
||||
// 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;");
|
||||
@@ -226,6 +248,32 @@ describe("FloatingWindow", () => {
|
||||
expect(cssRuleFor(phoneBlock, ".floating-window--task-detail .floating-window__resize-handle")).toContain("display: none;");
|
||||
});
|
||||
|
||||
/*
|
||||
FNXC:ModalTouchGeometry 2026-08-01-03:48:
|
||||
Tablet MODE is its own styling marker, distinct from `--touch-geometry`: a non-touch window
|
||||
at tablet widths must still receive `floating-window--tablet` so the FN-8015 gutter zeroing
|
||||
applies everywhere the app classifies the viewport as tablet.
|
||||
*/
|
||||
it("marks tablet-mode windows with floating-window--tablet even without touch", () => {
|
||||
vi.stubGlobal("matchMedia", vi.fn((query: string) => ({
|
||||
matches: query === "(min-width: 769px) and (max-width: 1024px)",
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
})));
|
||||
render(
|
||||
<FloatingWindow windowKey="tablet-mode" title="Tablet" onClose={() => {}}>
|
||||
<div>tablet body</div>
|
||||
</FloatingWindow>
|
||||
);
|
||||
const panel = screen.getByTestId("floating-window-tablet-mode");
|
||||
expect(panel.className).toContain("floating-window--tablet");
|
||||
});
|
||||
|
||||
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 { unmount } = render(
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user