From 15b5441afce0c75e8a857961f59159a008183624 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Thu, 23 Jul 2026 13:28:19 -0700 Subject: [PATCH] FN-8548: keep GitHub import actions on one mobile row Keep all selected GitHub issue actions touch-safe and visible on a single mobile row. - Group detail action controls separately from the comment composer and make their mobile tracks shrinkable. - Add responsive browser-smoke coverage at 320px, 390px, and 412px plus modal structure tests. - Document the mobile behavior and add a patch changeset. Files changed: .changeset/fn-8548-mobile-github-import-actions.md | 7 + docs/dashboard-guide.md | 8 +- .../dashboard/app/components/GitHubImportModal.css | 42 +++++- .../dashboard/app/components/GitHubImportModal.tsx | 80 ++++++------ .../__tests__/GitHubImportModal.test.tsx | 50 ++++++++ packages/dashboard/app/styles.css | 11 +- .../dashboard/scripts/browser-layout-smoke.mjs | 142 ++++++++++++++++++++- 7 files changed, 290 insertions(+), 50 deletions(-) Fusion-Task-Id: FN-8548 Fusion-Task-Lineage: 8fab6a1d-0ca9-4246-9707-e1fc84ca58e5 Co-authored-by: Fusion (runfusion.ai) --- .../fn-8548-mobile-github-import-actions.md | 7 + docs/dashboard-guide.md | 8 +- .../app/components/GitHubImportModal.css | 42 +++++- .../app/components/GitHubImportModal.tsx | 80 +++++----- .../__tests__/GitHubImportModal.test.tsx | 50 ++++++ packages/dashboard/app/styles.css | 11 +- .../scripts/browser-layout-smoke.mjs | 142 +++++++++++++++++- 7 files changed, 290 insertions(+), 50 deletions(-) create mode 100644 .changeset/fn-8548-mobile-github-import-actions.md diff --git a/.changeset/fn-8548-mobile-github-import-actions.md b/.changeset/fn-8548-mobile-github-import-actions.md new file mode 100644 index 0000000000..0239104d68 --- /dev/null +++ b/.changeset/fn-8548-mobile-github-import-actions.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Keep GitHub issue import actions on one usable mobile row. +category: fix +dev: Adds Blink geometry coverage for 320px, 390px, and 412px action bars. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 203180cb89..7d40bc5076 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -378,8 +378,14 @@ Use GitHub import on mobile: 1. Open the compact Header actions or bottom **More** sheet and select **Import from GitHub**. Expected outcome: the same import workflow opens in the mobile modal layout. + 2. Choose the repository, issue/PR tab, candidate row, and detail action. For GitHub issues, choose **Import as task** for direct tracked creation or **Plan** to start Planning Mode with the issue context. - Expected outcome: direct import creates the board task with the same GitHub provenance/tracking metadata as the desktop/tablet **Import Tasks** view; Plan opens the Planning Mode interview without source-issue tracking. + Expected outcome: direct import creates the board task with the same GitHub provenance/tracking metadata as the desktop/tablet **Import Tasks** view; Plan opens the Planning Mode interview without source-issue tracking. When all GitHub issue actions are available, their full labels remain on one touch-safe action row. 3. While a candidate detail sheet is open, use the platform Back gesture or control. Expected outcome: the first Back dismisses only the issue, pull request, or GitLab detail and returns to the import candidate list; a second Back dismisses the import form. diff --git a/packages/dashboard/app/components/GitHubImportModal.css b/packages/dashboard/app/components/GitHubImportModal.css index 5735ab4798..11e2140581 100644 --- a/packages/dashboard/app/components/GitHubImportModal.css +++ b/packages/dashboard/app/components/GitHubImportModal.css @@ -345,8 +345,16 @@ when the preview is short; the top border separates it from scrolling content. border-top: 1px solid var(--border); } +/* Keep the composer and its action controls as independently sized flex rows. */ +.github-import-detail-action-row { + display: flex; + align-items: center; + justify-content: flex-end; + gap: var(--space-sm); +} + /* Never let the primary action's label clip when the bar is narrow. */ -.github-import-detail-actions .btn { +.github-import-detail-action-row .btn { flex: 0 0 auto; min-width: max-content; min-height: 36px; @@ -1655,6 +1663,38 @@ counterpart added later cannot silently reintroduce the band. } +/* +FNXC:GitHubImport 2026-07-23-12:18: +The selected-issue mobile action row must keep Import as task alongside optional Close issue, +Plan, and Chat controls. The comment composer remains on its own full-width row, while equal +shrinkable action tracks preserve every full label by wrapping it inside its own button and the +shared touch-target token preserves every required target without clipping or scrolling. +*/ +@media (max-width: 768px) { + .github-import-detail-actions { + gap: var(--space-xs); + } + + .github-import-detail-action-row { + flex: 1 1 100%; + min-width: 0; + align-items: stretch; + flex-wrap: nowrap; + gap: var(--space-xs); + } + + .github-import-detail-action-row .btn { + flex: 1 1 0; + min-width: 0; + min-height: var(--touch-target-min-size) !important; + padding: var(--space-xs); + justify-content: center; + text-align: center; + white-space: normal; + overflow-wrap: anywhere; + } +} + @keyframes github-import-spinner-spin { to { transform: rotate(360deg); diff --git a/packages/dashboard/app/components/GitHubImportModal.tsx b/packages/dashboard/app/components/GitHubImportModal.tsx index 8d76b85c12..e4da378d74 100644 --- a/packages/dashboard/app/components/GitHubImportModal.tsx +++ b/packages/dashboard/app/components/GitHubImportModal.tsx @@ -2316,48 +2316,50 @@ export function GitHubImportModal({ isOpen, onClose, onImport, onPlanningMode, o )} - {activeTab === "issues" && selectedIssue && !selectedIssueClosed && ( +
+ {activeTab === "issues" && selectedIssue && !selectedIssueClosed && ( + + )} + {activeTab === "issues" && selectedIssue && onPlanningMode && ( + + )} + {onOpenChatWithPrefill && (activeTab === "issues" ? selectedIssue?.html_url?.trim() : selectedPull?.html_url?.trim()) && ( + + )} - )} - {activeTab === "issues" && selectedIssue && onPlanningMode && ( - - )} - {onOpenChatWithPrefill && (activeTab === "issues" ? selectedIssue?.html_url?.trim() : selectedPull?.html_url?.trim()) && ( - - )} - +
diff --git a/packages/dashboard/app/components/__tests__/GitHubImportModal.test.tsx b/packages/dashboard/app/components/__tests__/GitHubImportModal.test.tsx index 723c804cc7..924c00dc8b 100644 --- a/packages/dashboard/app/components/__tests__/GitHubImportModal.test.tsx +++ b/packages/dashboard/app/components/__tests__/GitHubImportModal.test.tsx @@ -3160,6 +3160,56 @@ describe("GitHubImportModal — detail actions sit at the bottom (operator repor }); }); + it("keeps the four populated issue actions uniquely ordered in the shared mobile bar", async () => { + vi.mocked(apiFetchGitHubIssues).mockResolvedValueOnce([ + { number: 44, title: "Four actions", body: "Body", html_url: "https://github.com/dustinbyrne/kb/issues/44", labels: [], state: "open" }, + ]); + render( + , + ); + fireEvent.click(await screen.findByRole("button", { name: /Select issue #44/i })); + + const bar = await screen.findByTestId("github-import-detail-actions"); + const actionRow = within(bar).getByTestId("github-import-detail-action-row"); + const composer = within(bar).getByTestId("github-import-issue-comment-input"); + const actions = [ + within(actionRow).getByTestId("github-import-issue-close"), + within(actionRow).getByTestId("github-import-action-plan"), + within(actionRow).getByTestId("github-import-action-chat"), + within(actionRow).getByTestId("github-import-action-top"), + ]; + expect(actions.map((action) => action.textContent?.trim())).toEqual(["Close issue", "Plan", "Chat", "Import as task"]); + expect(composer.closest(".github-import-detail-actions")).toBe(bar); + expect(actionRow.previousElementSibling).toBe(composer.closest("form")); + expect(within(actionRow).getAllByRole("button", { name: /^Close issue$/i })).toHaveLength(1); + expect(within(actionRow).getAllByRole("button", { name: /^Plan$/i })).toHaveLength(1); + expect(within(actionRow).getAllByRole("button", { name: /^Chat$/i })).toHaveLength(1); + expect(within(actionRow).getAllByRole("button", { name: /^Import as task$/i })).toHaveLength(1); + }); + + it("uses shrinkable, non-wrapping mobile action tracks while retaining desktop behavior", () => { + const source = readFileSync(resolve(__dirname, "../GitHubImportModal.css"), "utf8"); + const mobileRule = source.match(/@media\s\(max-width:\s768px\)\s\{[\s\S]*?\.github-import-detail-action-row\s\.btn\s\{[\s\S]*?\n\s{2}\}/)?.[0] ?? ""; + + expect(mobileRule).toContain(".github-import-detail-action-row {"); + expect(mobileRule).toContain("flex-wrap: nowrap;"); + expect(mobileRule).toContain("flex: 1 1 100%;"); + expect(mobileRule).toContain("gap: var(--space-xs);"); + expect(mobileRule).toContain("flex: 1 1 0;"); + expect(mobileRule).toContain("min-width: 0;"); + expect(mobileRule).toContain("min-height: var(--touch-target-min-size) !important;"); + expect(mobileRule).toContain("white-space: normal;"); + expect(mobileRule).toContain("overflow-wrap: anywhere;"); + expect(source.match(/\.github-import-detail-actions\s*\{[^}]*flex-wrap: wrap;/)?.[0]).toBeTruthy(); + }); + /* FNXC:GitHubImport 2026-07-17-12:00: diff --git a/packages/dashboard/app/styles.css b/packages/dashboard/app/styles.css index 062c5848c8..846cb6ae06 100644 --- a/packages/dashboard/app/styles.css +++ b/packages/dashboard/app/styles.css @@ -71,8 +71,8 @@ html { recommended by Apple HIG and WCAG 2.5.8. Apply to interactive elements that are too small on mobile (links, small buttons, icons). */ .touch-target { - min-width: 44px; - min-height: 44px; + min-width: var(--touch-target-min-size); + min-height: var(--touch-target-min-size); display: inline-flex; align-items: center; justify-content: center; @@ -146,6 +146,13 @@ html { --space-xl: 24px; --space-2xl: 32px; + /* + FNXC:DashboardStyling 2026-07-23-13:05: + Mobile action controls need one shared minimum hit-area token so compact layouts can retain the + 44px accessibility floor without embedding a raw dimension in component-specific rules. + */ + --touch-target-min-size: 44px; + /* FNXC:QuickAddActionRow 2026-07-16-14:00: FN-8147 pins Quick Add action-row control heights to literal values rather than diff --git a/packages/dashboard/scripts/browser-layout-smoke.mjs b/packages/dashboard/scripts/browser-layout-smoke.mjs index 9b150d2ebe..0d0e7bcd6e 100644 --- a/packages/dashboard/scripts/browser-layout-smoke.mjs +++ b/packages/dashboard/scripts/browser-layout-smoke.mjs @@ -1,10 +1,11 @@ #!/usr/bin/env node /* global WebSocket, URL, fetch, console, setTimeout, clearTimeout */ +import { Buffer } from "node:buffer"; import { spawn } from "node:child_process"; import { createServer } from "node:http"; import { superviseSpawn } from "@fusion/core"; -import { readFile, readdir, rm, stat, mkdtemp } from "node:fs/promises"; +import { readFile, readdir, rm, stat, mkdtemp, writeFile } from "node:fs/promises"; import { existsSync } from "node:fs"; import os from "node:os"; import path from "node:path"; @@ -15,6 +16,7 @@ const dashboardRoot = path.resolve(import.meta.dirname, ".."); const appRoot = path.join(dashboardRoot, "app"); const clientDistRoot = path.join(dashboardRoot, "dist", "client"); const requireBrowser = process.argv.includes("--require-browser") || process.env.FUSION_BROWSER_SMOKE_REQUIRE === "1"; +const screenshotPath = process.env.FUSION_BROWSER_SMOKE_SCREENSHOT; function log(message) { console.log(`[dashboard-browser-smoke] ${message}`); @@ -24,13 +26,15 @@ function fail(message) { throw new Error(message); } +/* +FNXC:GitHubImport 2026-07-23-13:05: +The mobile GitHub action-bar regression measures emitted CSS, so every smoke invocation must +rebuild the client before loading it. Reusing a stale dist/client artifact could make geometry +assertions pass against styles predating the one-row compact-layout requirement. +*/ async function loadDashboardCss() { - try { - return await readEmittedClientCss(); - } catch { - await runCommand("pnpm", ["--filter", "@fusion/dashboard", "build:client"], dashboardRoot); - return readEmittedClientCss(); - } + await runCommand("pnpm", ["--filter", "@fusion/dashboard", "build:client"], dashboardRoot); + return readEmittedClientCss(); } async function readEmittedClientCss() { @@ -188,6 +192,29 @@ export function createSmokeHtml() { measure the shared ViewHeader flex geometry. Exercise the unread Inbox's tightest badge + Compose + Mark all read + Refresh row and both Compose + Refresh-only states. */ + /* + FNXC:GitHubImport 2026-07-23-12:18: + FN-8548 mirrors the selected GitHub issue's four-action footer with the emitted dashboard CSS. + Blink must measure the production responsive contract at each supported phone width because jsdom + cannot detect wrapping, flex-track shrinkage, overflow, or touch-target geometry. + */ + const githubImportMobileActionFixture = ` +
+
+
+ + +
+
+ + + + +
+
+
+ `; + const mailboxMobileHeaderFixtures = [ ["unread-inbox", '9'], ["read-inbox", ''], @@ -288,6 +315,8 @@ export function createSmokeHtml() { ${mailboxMobileHeaderFixtures} + ${githubImportMobileActionFixture} +