diff --git a/.changeset/fn-8537-mobile-planning-actions.md b/.changeset/fn-8537-mobile-planning-actions.md new file mode 100644 index 0000000000..46e0f0ff2e --- /dev/null +++ b/.changeset/fn-8537-mobile-planning-actions.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Keep Planning Refine and Proceed actions visible on mobile. +category: fix +dev: Pin the plan action rail while its Markdown document scrolls in short and narrow viewports. diff --git a/packages/dashboard/app/components/PlanningModeModal.css b/packages/dashboard/app/components/PlanningModeModal.css index 6e0f74927f..c16a3fe3a9 100644 --- a/packages/dashboard/app/components/PlanningModeModal.css +++ b/packages/dashboard/app/components/PlanningModeModal.css @@ -2646,6 +2646,31 @@ mobile and desktop offer the same View task / Return to sessions choices. } } +/* +FNXC:PlanningModeMobile 2026-07-23-12:00: +Plan selection actions must be visible before document scrolling in phone portrait and width-independent short landscape. Keep this scoped to the review pane: its Markdown scroll sibling may shrink, but the existing action rail remains a non-shrinking, safe-area-padded footer in both embedded and dialog hosts. +*/ +@media (max-width: 768px), (max-height: 480px) { + .planning-plan-review, + .planning-plan-review > .planning-plan-pane { + min-height: 0; + } + + .planning-plan-review > .planning-plan-pane { + flex: 1 1 0; + } + + .planning-plan-pane > .planning-plan-scroll { + flex: 1 1 0; + min-height: 0; + overflow-y: auto; + } + + .planning-plan-pane > .planning-plan-actions { + flex: 0 0 auto; + } +} + /* === Planning & Breakdown Modal — Inline Style Replacements === */ /* Icon color utilities */ diff --git a/packages/dashboard/app/components/__tests__/PlanningModeModal.css.test.ts b/packages/dashboard/app/components/__tests__/PlanningModeModal.css.test.ts index fd14558930..62ba37f72c 100644 --- a/packages/dashboard/app/components/__tests__/PlanningModeModal.css.test.ts +++ b/packages/dashboard/app/components/__tests__/PlanningModeModal.css.test.ts @@ -176,6 +176,23 @@ describe("PlanningModeModal CSS responsive action contract", () => { expect(findRule(mobileCss, ".planning-add-comment--mobile")).toMatch(/margin-top\s*:\s*0\s*;/); }); + it("pins only the plan-selection rail while its document scrolls in portrait and width-independent short landscape", () => { + const css = loadPlanningCss(); + const responsiveCss = getMediaBlocks(css, MOBILE_PLANNING_SHELL_QUERY).join("\n"); + const boundedPaneRule = findRule(responsiveCss, ".planning-plan-review,\n .planning-plan-review > .planning-plan-pane"); + const shrinkablePaneRules = findRules(responsiveCss, ".planning-plan-review > .planning-plan-pane"); + const scrollOwnerRule = findRule(responsiveCss, ".planning-plan-pane > .planning-plan-scroll"); + const pinnedActionsRule = findRule(responsiveCss, ".planning-plan-pane > .planning-plan-actions"); + + expect(boundedPaneRule).toMatch(/min-height\s*:\s*0\s*;/); + expect(shrinkablePaneRules.some((rule) => /flex\s*:\s*1 1 0\s*;/.test(rule))).toBe(true); + expect(scrollOwnerRule).toMatch(/flex\s*:\s*1 1 0\s*;/); + expect(scrollOwnerRule).toMatch(/min-height\s*:\s*0\s*;/); + expect(scrollOwnerRule).toMatch(/overflow-y\s*:\s*auto\s*;/); + expect(pinnedActionsRule).toMatch(/flex\s*:\s*0 0 auto\s*;/); + expect(responsiveCss).not.toMatch(/\.planning-actions\s*>\s*\.planning-plan-actions/); + }); + it("keeps the mobile sessions list scrolling above the bottom-pinned New session footer", () => { const css = loadPlanningCss(); const mobileShellCss = getMediaBlocks(css, MOBILE_PLANNING_SHELL_QUERY).join("\n"); diff --git a/packages/dashboard/src/__tests__/planning-browser-e2e.test.ts b/packages/dashboard/src/__tests__/planning-browser-e2e.test.ts index 83401413ee..258c6733d3 100644 --- a/packages/dashboard/src/__tests__/planning-browser-e2e.test.ts +++ b/packages/dashboard/src/__tests__/planning-browser-e2e.test.ts @@ -110,13 +110,16 @@ describe.runIf(executablePath)("Planning Mode browser E2E", () => { await page.close(); }, 30_000); - async function verifyResponsiveWorkspace(viewport: { width: number; height: number }, mobile: boolean): Promise { + async function verifyResponsiveWorkspace(viewport: { width: number; height: number }, mobile: boolean, presentation: "embedded" | "modal" = "embedded"): Promise { const page = await browser.newPage({ viewport }); - await page.goto(`${baseUrl}app/planning-browser-e2e-fixture.html?surface=plan-review&reset=1`); + await page.goto(`${baseUrl}app/planning-browser-e2e-fixture.html?surface=plan-review&presentation=${presentation}&reset=1`); if (mobile) await page.getByRole("tab", { name: "Plan preview" }).click(); await expectVisible(page.locator("[data-testid='planning-plan-markdown'] h1")); if (!mobile) await expectVisible(page.getByText("Which user outcome matters most?")); await expectVisible(page.getByRole("button", { name: "Proceed with plan" })); + if (process.env.FUSION_CAPTURE_DIR && ((viewport.width === 390 && viewport.height === 844) || (viewport.width === 844 && viewport.height === 390))) { + await page.screenshot({ path: `${process.env.FUSION_CAPTURE_DIR}/planning-actions-${presentation}-${viewport.width}x${viewport.height}.png` }); + } const layout = await page.evaluate(() => { const workspace = document.querySelector("[data-testid='planning-workspace']")!; @@ -149,6 +152,9 @@ describe.runIf(executablePath)("Planning Mode browser E2E", () => { && Math.abs(actionsRect.bottom - questionActionsRect.bottom) <= 1, actionTopDelta: Math.round(Math.abs(actionsRect.top - questionActionsRect.top)), actionBottomDelta: Math.round(Math.abs(actionsRect.bottom - questionActionsRect.bottom)), + actionsInsideViewport: actionsRect.top >= 0 && actionsRect.bottom <= window.innerHeight, + refineVisible: [...actions.querySelectorAll("button")].some((button) => button.textContent?.trim() === "Refine" && button.getBoundingClientRect().width > 0 && button.getBoundingClientRect().height > 0), + proceedVisible: [...actions.querySelectorAll("button")].some((button) => button.textContent?.trim() === "Proceed with plan" && button.getBoundingClientRect().width > 0 && button.getBoundingClientRect().height > 0), }; }); @@ -168,6 +174,9 @@ describe.runIf(executablePath)("Planning Mode browser E2E", () => { desktopActionRowsAligned: mobile ? false : true, actionTopDelta: mobile ? expect.any(Number) : 0, actionBottomDelta: mobile ? expect.any(Number) : 0, + actionsInsideViewport: true, + refineVisible: true, + proceedVisible: true, }); await page.close(); } @@ -175,6 +184,13 @@ describe.runIf(executablePath)("Planning Mode browser E2E", () => { it("keeps the Markdown plan right of the question on desktop", () => verifyResponsiveWorkspace({ width: 1440, height: 900 }, false), 30_000); it("keeps the Markdown plan reachable through the mobile workspace tab", () => verifyResponsiveWorkspace({ width: 390, height: 568 }, true), 30_000); + it("keeps plan selection actions visible before scroll in portrait and short landscape across hosts", async () => { + for (const presentation of ["embedded", "modal"] as const) { + await verifyResponsiveWorkspace({ width: 390, height: 844 }, true, presentation); + await verifyResponsiveWorkspace({ width: 844, height: 390 }, false, presentation); + } + }, 30_000); + async function selectPlanQuote(page: Page): Promise { await page.evaluate(() => { const markdown = document.querySelector("[data-testid='planning-plan-markdown']")!;