diff --git a/.changeset/fn-8722-github-import-mobile-spacing.md b/.changeset/fn-8722-github-import-mobile-spacing.md new file mode 100644 index 0000000000..be4075175f --- /dev/null +++ b/.changeset/fn-8722-github-import-mobile-spacing.md @@ -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. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index e0d2e21eec..20474049b1 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -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. -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. + +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. ### Dashboard modal inventory diff --git a/packages/dashboard/app/__tests__/browser-layout-smoke-fixture.test.ts b/packages/dashboard/app/__tests__/browser-layout-smoke-fixture.test.ts index 68d7da17f0..571bbaf648 100644 --- a/packages/dashboard/app/__tests__/browser-layout-smoke-fixture.test.ts +++ b/packages/dashboard/app/__tests__/browser-layout-smoke-fixture.test.ts @@ -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"'); diff --git a/packages/dashboard/app/components/FloatingWindow.css b/packages/dashboard/app/components/FloatingWindow.css index dffeb46d6e..e886df6196 100644 --- a/packages/dashboard/app/components/FloatingWindow.css +++ b/packages/dashboard/app/components/FloatingWindow.css @@ -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; diff --git a/packages/dashboard/app/components/__tests__/GitHubImportModal.test.tsx b/packages/dashboard/app/components/__tests__/GitHubImportModal.test.tsx index 6e34c78001..1c71597523 100644 --- a/packages/dashboard/app/components/__tests__/GitHubImportModal.test.tsx +++ b/packages/dashboard/app/components/__tests__/GitHubImportModal.test.tsx @@ -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)"); diff --git a/packages/dashboard/scripts/browser-layout-smoke.mjs b/packages/dashboard/scripts/browser-layout-smoke.mjs index 433b56c593..70d1e410ec 100644 --- a/packages/dashboard/scripts/browser-layout-smoke.mjs +++ b/packages/dashboard/scripts/browser-layout-smoke.mjs @@ -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() { `; + /* + 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 = ` +
+
+ +
+ +
+
+ +
+

Issue detail

Detail control fixture
`; + return ` @@ -266,6 +296,7 @@ export function createSmokeHtml() {
${gitManagerFixtures} + ${gitHubImportFixtures}
@@ -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,