FN-8722: fix GitHub Import sheet spacing

Fix standalone GitHub Import content alignment on mobile and short sheet viewports.

- Remove the standalone resize-handle gutter when the FloatingWindow sheet contract applies.
- Add fixture and browser-smoke coverage for standalone, embedded, and detail import layouts.
- Document the shared short-height sheet geometry behavior and add a patch changeset.

Files changed:
 .changeset/fn-8722-github-import-mobile-spacing.md |  7 ++
 docs/dashboard-guide.md                            |  3 +-
 .../__tests__/browser-layout-smoke-fixture.test.ts | 17 +++++
 .../dashboard/app/components/FloatingWindow.css    | 12 ++++
 .../__tests__/GitHubImportModal.test.tsx           | 18 +++++
 .../dashboard/scripts/browser-layout-smoke.mjs     | 78 ++++++++++++++++++++++
 6 files changed, 134 insertions(+), 1 deletion(-)

Fusion-Task-Id: FN-8722

Fusion-Task-Lineage: cc812078-b0b5-4223-9435-ed77d80f2b47

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-08-01 20:04:41 -07:00
parent fab2b3a7b1
commit 4fb873f465
6 changed files with 134 additions and 1 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Fix GitHub Import sheet spacing on phones and short viewports.
category: fix
dev: Clears the standalone FloatingWindow resize gutter only while its sheet contract hides handles.

View File

@@ -87,7 +87,8 @@ Every configured action toggles its interface: press once to open Quick Chat, Te
Press `Escape` to close the current/topmost dashboard popup. Popped-out task windows and floating Quick Chat close before fixed app modals such as Terminal, Settings, Files, or Task Detail, and only one surface closes per key press. Nested editors and menus that already handle Escape keep first ownership by preventing the global handler.
<!-- FNXC:ModalGeometryPersistenceDocs 2026-07-16-00:40: Full-screen mobile FloatingWindow sheets must preserve, rather than overwrite, the movable desktop geometry record so a later desktop reopen restores the user's chosen location and size. -->
Movable dashboard pop-outs remember their last desktop location and size, while centered resizable dialogs remember their size. When a pop-out becomes a full-screen sheet at mobile widths (or, for Artifact Gallery, its short-height sheet breakpoint), it leaves that desktop record untouched; reopening it on desktop restores the prior floating geometry.
<!-- FNXC:GitHubImport 2026-08-02-02:51: FN-8722 confirms that standalone GitHub Import also uses the canonical width-or-height sheet contract, so the operator guide must not describe short-sheet preservation as Artifact Gallery-only. -->
Movable dashboard pop-outs remember their last desktop location and size, while centered resizable dialogs remember their size. When a pop-out becomes a full-screen sheet at mobile widths or its configured short-height breakpoint, it leaves that desktop record untouched; reopening it on desktop restores the prior floating geometry.
<!-- FNXC:TaskModalResizeDocs 2026-07-26-15:55: Known touch tablets at the 768px CSS boundary use the shared physical-screen-aware viewport classification, so documentation must distinguish their resize contract from true phones that share the CSS media query. Tablet target expansion is hit-area-only and must never add a visible panel inset. -->
### Dashboard modal inventory

View File

@@ -25,6 +25,23 @@ describe("browser layout smoke fixture", () => {
expect(html).toContain("gm-modal--embedded");
});
it("includes standalone, embedded, and detail GitHub Import shell fixtures", () => {
const html = createSmokeHtml();
for (const hook of [
"github-import-standalone", "github-import-standalone-body", "github-import-standalone-modal",
"github-import-standalone-header", "github-import-standalone-close", "github-import-standalone-controls",
"github-import-standalone-list", "github-import-standalone-pagination", "github-import-standalone-footer",
"github-import-embedded-host", "github-import-embedded-modal", "github-import-embedded-header",
"github-import-embedded-content", "github-import-detail", "github-import-detail-body",
"github-import-detail-panel", "github-import-detail-close",
]) {
expect(html).toContain(`data-smoke="${hook}"`);
}
expect(html).toContain("floating-window--github-import");
expect(html).toContain("github-import-modal--embedded");
expect(html).toContain("floating-window--github-import-detail");
});
it("includes PR flow fixture sections and class hooks", () => {
const html = createSmokeHtml();
expect(html).toContain('data-smoke="pr-create-modal"');

View File

@@ -482,6 +482,18 @@ all floating affordances so persisted desktop geometry cannot imply a draggable
margin-inline-end: 0;
}
/*
FNXC:GitHubImport 2026-08-02-02:47:
FN-8722 makes the standalone GitHub Import host follow the full width-or-height
sheet contract. Its shared body gutter exists only to clear desktop resize handles;
once this predicate hides those handles, retaining the gutter shifts every shell
region left. Do not broaden this rule to embedded Import Tasks or the independent
detail window, and keep non-short tablet/desktop resize clearance intact.
*/
.floating-window--github-import .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;

View File

@@ -69,6 +69,8 @@ vi.mock("../../api", async (importOriginal) => {
};
});
const floatingWindowCss = readFileSync(resolve(__dirname, "../FloatingWindow.css"), "utf8");
const mockTask: Task = {
id: "FN-001",
title: "Test Issue",
@@ -118,6 +120,22 @@ describe("GitHubImportModal", () => {
const onClose = vi.fn();
const onImport = vi.fn();
/*
FNXC:GitHubImport 2026-08-02-02:47:
This stylesheet guard complements the emitted-CSS Chromium geometry regression: only the
standalone importer clears the shared gutter under FloatingWindow's canonical sheet predicate.
*/
it("clears the standalone sheet gutter without changing embedded or detail presentations", () => {
const sheetStart = floatingWindowCss.indexOf("@media (max-width: 767.98px), (max-height: 480px)");
const sheetEnd = floatingWindowCss.indexOf("@media (max-width: 767.98px) {", sheetStart + 1);
const sheetStyles = floatingWindowCss.slice(sheetStart, sheetEnd);
expect(sheetStyles).toContain(".floating-window--github-import .floating-window__body {");
expect(sheetStyles).toContain("margin-inline-end: 0;");
expect(sheetStyles).not.toContain(".floating-window--github-import-detail .floating-window__body");
expect(sheetStyles).not.toContain(".github-import-embedded");
});
it("uses color-mix tokens for focus and selection surfaces", () => {
const source = readFileSync(resolve(__dirname, "../GitHubImportModal.css"), "utf8");
expect(source).not.toContain("rgba(var(--color-primary-rgb)");

View File

@@ -21,6 +21,9 @@ const agentHeartbeatMobileScreenshotPath = process.env.FUSION_AGENT_HEARTBEAT_MO
const agentHeartbeatDesktopScreenshotPath = process.env.FUSION_AGENT_HEARTBEAT_DESKTOP_SCREENSHOT;
const gitManagerBeforeMobileScreenshotPath = process.env.FUSION_GIT_MANAGER_BEFORE_MOBILE_SCREENSHOT;
const gitManagerAfterMobileScreenshotPath = process.env.FUSION_GIT_MANAGER_AFTER_MOBILE_SCREENSHOT;
const gitHubImportBeforeMobileScreenshotPath = process.env.FUSION_GITHUB_IMPORT_BEFORE_MOBILE_SCREENSHOT;
const gitHubImportAfterMobileScreenshotPath = process.env.FUSION_GITHUB_IMPORT_AFTER_MOBILE_SCREENSHOT;
const gitHubImportAfterShortScreenshotPath = process.env.FUSION_GITHUB_IMPORT_AFTER_SHORT_SCREENSHOT;
function log(message) {
console.log(`[dashboard-browser-smoke] ${message}`);
@@ -255,6 +258,33 @@ export function createSmokeHtml() {
</section>
</section>`;
/*
FNXC:GitHubImport 2026-08-02-02:45:
FN-8722 mirrors the standalone FloatingWindow chain rather than a generic overlay so Chromium
measures the inherited resize-handle gutter on the real sheet host. The fixture includes the
header/close control, controls, populated list, pagination, and footer plus embedded and detail
controls; all states must remain horizontally contained without changing their own geometry.
*/
const gitHubImportFixtures = `
<section class="floating-window floating-window--github-import" data-smoke="github-import-standalone" style="width: min(1200px, calc(100vw - var(--space-2xl))); height: min(720px, calc(100dvh - var(--space-2xl)));">
<div class="floating-window__body" data-smoke="github-import-standalone-body">
<section class="modal modal-lg github-import-modal" data-smoke="github-import-standalone-modal">
<header class="modal-header github-import-modal__header" data-smoke="github-import-standalone-header"><div><h3>Import from GitHub</h3><p class="github-import-modal__subtitle">Load issues or pull requests from the selected repository.</p></div><button class="modal-close" data-smoke="github-import-standalone-close" type="button" aria-label="Close import modal">×</button></header>
<div class="modal-body github-import-modal__body" data-smoke="github-import-standalone-content">
<div class="github-import-controls" data-smoke="github-import-standalone-controls"><div class="github-import-provider"><button class="github-import-tab active" type="button">GitHub</button></div><div class="github-import-tabs"><button class="github-import-tab active" type="button">Issues</button><button class="github-import-tab" type="button">Pull requests</button></div><div class="github-import-toolbar"><div class="github-import-toolbar__zone github-import-toolbar__zone--remote"><span class="github-import-remote-pill"><span class="github-import-remote-pill__name">origin</span><span class="github-import-remote-pill__repo">owner/repository</span></span></div><div class="github-import-toolbar__zone github-import-toolbar__zone--filter"><button class="btn github-import-filter-trigger" type="button">Filter</button></div><div class="github-import-toolbar__zone github-import-toolbar__zone--action"><button class="btn btn-primary github-import-load-button" type="button">Load</button></div></div></div>
<section class="github-import-list-pane" data-smoke="github-import-standalone-list"><header class="github-import-pane-header"><h4>Issues</h4><button class="modal-close" type="button" aria-label="List action">×</button></header><div class="github-import-pane-content"><div class="issues-list"><button class="issue-item" type="button"><span class="issue-main"><span class="issue-heading-row"><span class="issue-number">#8722</span><span class="issue-title">A deliberately long populated GitHub issue title that must stay inside the import sheet</span></span></span></button><button class="issue-item imported" type="button">Already imported issue</button></div></div></section>
<nav class="github-import-pagination" data-smoke="github-import-standalone-pagination"><button class="btn" type="button">Previous</button><button class="btn" type="button">Next</button></nav>
</div>
<footer class="modal-actions github-import-modal__actions" data-smoke="github-import-standalone-footer"><button class="btn" type="button">Cancel</button><button class="btn btn-primary" type="button">Import as task</button></footer>
</section>
</div>
<i class="floating-window__resize-handle floating-window__resize-handle--se" aria-hidden="true"></i>
</section>
<section class="github-import-embedded" data-smoke="github-import-embedded-host" style="width: min(320px, calc(100vw - var(--space-lg))); height: 420px;">
<section class="modal modal-lg github-import-modal github-import-modal--embedded" data-smoke="github-import-embedded-modal"><header class="github-import-modal__embedded-header" data-smoke="github-import-embedded-header"><h2 class="github-import-modal__embedded-title">Import Tasks</h2></header><div class="github-import-modal__body" data-smoke="github-import-embedded-content"><div class="github-import-state github-import-state--empty">Embedded empty state</div></div></section>
</section>
<section class="floating-window floating-window--github-import-detail" data-smoke="github-import-detail" style="width: min(760px, calc(100vw - var(--space-2xl))); height: min(680px, calc(100dvh - var(--space-2xl)));"><div class="floating-window__body" data-smoke="github-import-detail-body"><section class="github-import-detail-panel" data-smoke="github-import-detail-panel"><header class="github-import-pane-header"><h4>Issue detail</h4><button class="modal-close" data-smoke="github-import-detail-close" type="button" aria-label="Close detail">×</button></header><div class="github-import-pane-content">Detail control fixture</div></section></div><i class="floating-window__resize-handle floating-window__resize-handle--se" aria-hidden="true"></i></section>`;
return `<!doctype html>
<html lang="en">
<head>
@@ -266,6 +296,7 @@ export function createSmokeHtml() {
<body data-theme="dark">
<div id="root">
${gitManagerFixtures}
${gitHubImportFixtures}
<div class="header-wrapper">
<header class="header" data-smoke="header">
<div class="header-left">
@@ -1278,6 +1309,53 @@ async function runSmokeChecks(page, pageUrl) {
assertSmokeResult(`Git Manager standalone and embedded geometry at ${width}px`, passed, JSON.stringify(gitManagerLayout));
}
/*
FNXC:GitHubImport 2026-08-02-02:45:
FN-8722 requires both branches of FloatingWindow's width-or-height sheet predicate. Positive
assertions cover the real standalone regions at phone, wide-short, and desktop-short viewports,
while non-short 768px/desktop retain the desktop gutter and visible resize handle. Embedded and
detail fixtures are controls: they remain contained but receive no standalone-only reset.
*/
for (const [width, height] of [[390, 844], [768, 480], [1024, 480], [768, 844], [1024, 844]]) {
const isSheet = width < 768 || height <= 480;
await page.send("Emulation.setDeviceMetricsOverride", { width, height, deviceScaleFactor: width < 768 ? 2 : 1, mobile: width < 768 });
await evaluate(page, "document.fonts ? document.fonts.ready.then(() => true) : true");
const layout = await evaluate(page, `(() => {
const viewportWidth = window.innerWidth;
const readRect = (selector) => { const rect = document.querySelector(selector).getBoundingClientRect(); return { left: rect.left, right: rect.right, width: rect.width, top: rect.top, bottom: rect.bottom }; };
const standalone = document.querySelector('[data-smoke="github-import-standalone"]');
const standaloneBody = document.querySelector('[data-smoke="github-import-standalone-body"]');
const embeddedHost = document.querySelector('[data-smoke="github-import-embedded-host"]');
const detail = document.querySelector('[data-smoke="github-import-detail"]');
return { viewportWidth, documentOverflow: document.documentElement.scrollWidth - viewportWidth, standalone: { host: readRect('[data-smoke="github-import-standalone"]'), body: readRect('[data-smoke="github-import-standalone-body"]'), modal: readRect('[data-smoke="github-import-standalone-modal"]'), header: readRect('[data-smoke="github-import-standalone-header"]'), close: readRect('[data-smoke="github-import-standalone-close"]'), controls: readRect('[data-smoke="github-import-standalone-controls"]'), list: readRect('[data-smoke="github-import-standalone-list"]'), pagination: readRect('[data-smoke="github-import-standalone-pagination"]'), footer: readRect('[data-smoke="github-import-standalone-footer"]'), bodyMarginInlineEnd: getComputedStyle(standaloneBody).marginInlineEnd, overflow: standalone.scrollWidth - standalone.clientWidth, resizeHandleDisplay: getComputedStyle(standalone.querySelector('.floating-window__resize-handle')).display }, embedded: { host: readRect('[data-smoke="github-import-embedded-host"]'), modal: readRect('[data-smoke="github-import-embedded-modal"]'), header: readRect('[data-smoke="github-import-embedded-header"]'), content: readRect('[data-smoke="github-import-embedded-content"]'), overflow: embeddedHost.scrollWidth - embeddedHost.clientWidth }, detail: { host: readRect('[data-smoke="github-import-detail"]'), body: readRect('[data-smoke="github-import-detail-body"]'), panel: readRect('[data-smoke="github-import-detail-panel"]'), close: readRect('[data-smoke="github-import-detail-close"]'), overflow: detail.scrollWidth - detail.clientWidth } };
})()`);
if (width === 390 && gitHubImportBeforeMobileScreenshotPath) {
const preFixLayout = await evaluate(page, `(() => { const body = document.querySelector('[data-smoke="github-import-standalone-body"]'); body.style.marginInlineEnd = 'var(--space-lg)'; const rect = body.getBoundingClientRect(); return { right: rect.right, marginInlineEnd: getComputedStyle(body).marginInlineEnd, viewportWidth: window.innerWidth }; })()`);
assertSmokeResult("GitHub Import 390px desktop-gutter reproduction", parseFloat(preFixLayout.marginInlineEnd) > 0 && preFixLayout.right < preFixLayout.viewportWidth - 1, JSON.stringify(preFixLayout));
const screenshot = await page.send("Page.captureScreenshot", { format: "png" });
await writeFile(gitHubImportBeforeMobileScreenshotPath, Buffer.from(screenshot.data, "base64"));
await evaluate(page, "document.querySelector('[data-smoke=\"github-import-standalone-body\"]').style.removeProperty('margin-inline-end')");
log(`saved GitHub Import before mobile screenshot to ${gitHubImportBeforeMobileScreenshotPath}`);
}
if (isSheet && ((width === 390 && gitHubImportAfterMobileScreenshotPath) || (width === 768 && height === 480 && gitHubImportAfterShortScreenshotPath))) {
const screenshot = await page.send("Page.captureScreenshot", { format: "png" });
const target = width === 390 ? gitHubImportAfterMobileScreenshotPath : gitHubImportAfterShortScreenshotPath;
await writeFile(target, Buffer.from(screenshot.data, "base64"));
log(`saved GitHub Import sheet screenshot to ${target}`);
}
const standaloneRects = [layout.standalone.body, layout.standalone.modal, layout.standalone.header, layout.standalone.close, layout.standalone.controls, layout.standalone.list, layout.standalone.pagination, layout.standalone.footer];
const embeddedRects = [layout.embedded.modal, layout.embedded.header, layout.embedded.content];
const detailRects = [layout.detail.body, layout.detail.panel, layout.detail.close];
const standaloneContained = standaloneRects.every((rect) => rect.left >= -1 && rect.right <= width + 1);
const embeddedContained = embeddedRects.every((rect) => rect.left >= layout.embedded.host.left - 1 && rect.right <= layout.embedded.host.right + 1);
const detailContained = detailRects.every((rect) => rect.left >= -1 && rect.right <= width + 1);
const common = layout.documentOverflow <= 1 && layout.standalone.overflow <= 1 && layout.embedded.overflow <= 1 && layout.detail.overflow <= 1 && standaloneContained && embeddedContained && detailContained && layout.standalone.close.right <= layout.standalone.header.right + 1 && layout.detail.close.right <= layout.detail.panel.right + 1;
const passed = isSheet
? common && Math.abs(layout.standalone.host.left) <= 1 && Math.abs(layout.standalone.host.right - width) <= 1 && Math.abs(layout.standalone.body.right - width) <= 1 && Math.abs(layout.standalone.modal.right - width) <= 1 && layout.standalone.bodyMarginInlineEnd === "0px" && layout.standalone.resizeHandleDisplay === "none"
: common && layout.standalone.host.width < width - 1 && parseFloat(layout.standalone.bodyMarginInlineEnd) > 0 && layout.standalone.resizeHandleDisplay !== "none";
assertSmokeResult(`GitHub Import standalone, embedded, and detail geometry at ${width}x${height}`, passed, JSON.stringify(layout));
}
await page.send("Emulation.setDeviceMetricsOverride", {
width: 390,
height: 844,