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:
7
.changeset/fn-8537-mobile-planning-actions.md
Normal file
7
.changeset/fn-8537-mobile-planning-actions.md
Normal 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.
|
||||
@@ -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 */
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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']")!;
|
||||
|
||||
Reference in New Issue
Block a user