FN-8537: keep planning actions visible on mobile

Keep Planning Refine and Proceed controls visible across mobile plan-review hosts.

- Make the plan document pane the responsive scroll owner while preserving its action rail.
- Cover portrait and short-landscape embedded and modal layouts with CSS and browser tests.
- Add a patch changeset for the mobile planning action fix.

Files changed:
 .changeset/fn-8537-mobile-planning-actions.md      |  7 ++++++
 .../dashboard/app/components/PlanningModeModal.css | 25 ++++++++++++++++++++++
 .../__tests__/PlanningModeModal.css.test.ts        | 17 +++++++++++++++
 .../src/__tests__/planning-browser-e2e.test.ts     | 20 +++++++++++++++--
 4 files changed, 67 insertions(+), 2 deletions(-)

Fusion-Task-Id: FN-8537

Fusion-Task-Lineage: 7a53aa74-859d-4c76-bf26-ab6ea72873dd

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-23 11:03:33 -07:00
parent 2499803c73
commit d6d8a5e919
4 changed files with 67 additions and 2 deletions

View File

@@ -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.

View File

@@ -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 */

View File

@@ -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");

View File

@@ -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<void> {
async function verifyResponsiveWorkspace(viewport: { width: number; height: number }, mobile: boolean, presentation: "embedded" | "modal" = "embedded"): Promise<void> {
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<HTMLElement>("[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<HTMLButtonElement>("button")].some((button) => button.textContent?.trim() === "Refine" && button.getBoundingClientRect().width > 0 && button.getBoundingClientRect().height > 0),
proceedVisible: [...actions.querySelectorAll<HTMLButtonElement>("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<void> {
await page.evaluate(() => {
const markdown = document.querySelector<HTMLElement>("[data-testid='planning-plan-markdown']")!;