FN-7317: fix mobile planning summary controls

Fix mobile Planning Mode summary controls so description actions stay tappable and accessible.

- Move Markdown and Expand/Collapse controls outside the Description label to avoid mobile tap retargeting.\n- Add accessible labels and shared header/action styling for summary description controls.\n- Cover mobile expand/collapse behavior alongside the markdown preview toggle.\n- Add a patch changeset for the published CLI package.\n\nFiles changed:\n .changeset/fn-7317-mobile-planning-summary.md      |  7 +++\n .../dashboard/app/components/PlanningModeModal.css | 26 ++++++++-\n .../dashboard/app/components/PlanningModeModal.tsx | 58 +++++++++++--------\n .../PlanningModeModal.ui-interactions.test.tsx     | 65 ++++++++++++++++++++--\n 4 files changed, 128 insertions(+), 28 deletions(-)

Fusion-Task-Id: FN-7317

Fusion-Task-Lineage: f13bc27a-ea4e-46cd-aebb-2fe0a3251496

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-06-30 14:53:14 -07:00
parent 21fc28698f
commit 9f7215851b
4 changed files with 128 additions and 28 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Fix mobile Planning Mode summary description Expand and Collapse controls.
category: fix
dev: Splits the summary description label from adjacent Markdown and Expand controls and adds mobile regression coverage.

View File

@@ -1127,13 +1127,25 @@ An empty footer must NOT reserve vertical space or paint its divider band. When
gap: var(--space-sm); gap: var(--space-sm);
} }
.planning-expand-btn { .planning-summary-description-header {
display: flex;
align-items: center;
gap: var(--space-sm);
}
.planning-summary-description-actions {
display: flex;
align-items: center;
gap: var(--space-md);
margin-left: auto; margin-left: auto;
}
.planning-expand-btn {
padding: 0; padding: 0;
background: none; background: none;
border: none; border: none;
color: var(--todo); color: var(--todo);
font-size: 12px; font-size: calc(var(--space-sm) + var(--space-xs));
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
} }
@@ -1561,6 +1573,16 @@ Tablet embedded Planning keeps the desktop two-pane shell, so the summary footer
grid-template-columns: minmax(0, 1fr); grid-template-columns: minmax(0, 1fr);
} }
.planning-summary-description-header {
align-items: flex-start;
flex-wrap: wrap;
}
.planning-summary-description-actions {
margin-left: 0;
margin-inline-start: auto;
}
.planning-example-chips { .planning-example-chips {
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;

View File

@@ -2819,33 +2819,47 @@ function SummaryView({
<div className="planning-summary-form"> <div className="planning-summary-form">
<div className="form-group"> <div className="form-group">
<label> {/*
{t("planning.description", "Description")} FNXC:PlanningSummaryMobile 2026-06-30-12:00:
<button Keep Description controls adjacent but outside the label. Mobile browsers can retarget taps from interactive descendants inside labels, which made the Expand/Collapse control appear inert while the textarea/preview state already supported expansion.
type="button" */}
className="planning-expand-btn" <div className="planning-summary-description-header">
aria-pressed={renderMarkdown} <label id="planning-summary-description-label" htmlFor="planning-summary-description">
aria-label={renderMarkdown ? t("planning.showRawText", "Show raw text") : t("planning.showFormattedMarkdown", "Show formatted markdown")} {t("planning.description", "Description")}
title={renderMarkdown ? t("planning.showRawText", "Show raw text") : t("planning.showFormattedMarkdown", "Show formatted markdown")} </label>
data-testid="planning-description-markdown-toggle" <div className="planning-summary-description-actions">
onClick={() => setRenderMarkdown(!renderMarkdown)} <button
> type="button"
{renderMarkdown ? t("planning.plain", "Plain") : t("planning.markdown", "Markdown")} className="planning-expand-btn"
</button> aria-pressed={renderMarkdown}
<button aria-label={renderMarkdown ? t("planning.showRawText", "Show raw text") : t("planning.showFormattedMarkdown", "Show formatted markdown")}
type="button" title={renderMarkdown ? t("planning.showRawText", "Show raw text") : t("planning.showFormattedMarkdown", "Show formatted markdown")}
className="planning-expand-btn" data-testid="planning-description-markdown-toggle"
onClick={() => setIsExpanded(!isExpanded)} onClick={() => setRenderMarkdown(!renderMarkdown)}
> >
{isExpanded ? t("planning.collapse", "Collapse") : t("planning.expand", "Expand")} {renderMarkdown ? t("planning.plain", "Plain") : t("planning.markdown", "Markdown")}
</button> </button>
</label> <button
type="button"
className="planning-expand-btn"
aria-pressed={isExpanded}
aria-label={isExpanded ? t("planning.collapseDescription", "Collapse description") : t("planning.expandDescription", "Expand description")}
onClick={() => setIsExpanded(!isExpanded)}
>
{isExpanded ? t("planning.collapse", "Collapse") : t("planning.expand", "Expand")}
</button>
</div>
</div>
{renderMarkdown ? ( {renderMarkdown ? (
<div className={`planning-description-preview markdown-body ${isExpanded ? "expanded" : ""}`}> <div
className={`planning-description-preview markdown-body ${isExpanded ? "expanded" : ""}`}
aria-labelledby="planning-summary-description-label"
>
<ReactMarkdown remarkPlugins={[remarkGfm]}>{summary.description}</ReactMarkdown> <ReactMarkdown remarkPlugins={[remarkGfm]}>{summary.description}</ReactMarkdown>
</div> </div>
) : ( ) : (
<textarea <textarea
id="planning-summary-description"
ref={descriptionAutosizeRef} ref={descriptionAutosizeRef}
className={`planning-textarea ${isExpanded ? "expanded" : ""}`} className={`planning-textarea ${isExpanded ? "expanded" : ""}`}
value={summary.description} value={summary.description}

View File

@@ -104,6 +104,7 @@ vi.mock("../../api", () => ({
rejectPlan: (...args: any[]) => mockRejectPlan(...args), rejectPlan: (...args: any[]) => mockRejectPlan(...args),
refineTask: (...args: any[]) => mockRefineTask(...args), refineTask: (...args: any[]) => mockRefineTask(...args),
fetchSettings: vi.fn().mockResolvedValue({ modelPresets: [], autoSelectModelPreset: false, defaultPresetBySize: {} }), fetchSettings: vi.fn().mockResolvedValue({ modelPresets: [], autoSelectModelPreset: false, defaultPresetBySize: {} }),
fetchTaskEffectiveSettings: vi.fn().mockResolvedValue({ modelPresets: [], autoSelectModelPreset: false, defaultPresetBySize: {} }),
fetchGlobalSettings: vi.fn().mockResolvedValue({}), fetchGlobalSettings: vi.fn().mockResolvedValue({}),
fetchModels: (...args: any[]) => mockFetchModels(...args), fetchModels: (...args: any[]) => mockFetchModels(...args),
fetchWorkflowSteps: vi.fn().mockResolvedValue([]), fetchWorkflowSteps: vi.fn().mockResolvedValue([]),
@@ -213,7 +214,8 @@ describe("PlanningModeModal", () => {
/> />
); );
expect(screen.getByText("Definition")).toBeDefined(); expect(screen.getByText("Example task")).toBeDefined();
expect(screen.getByRole("button", { name: "Plan" })).toBeDefined();
expect(container.querySelector(".detail-body")).not.toBeNull(); expect(container.querySelector(".detail-body")).not.toBeNull();
}); });
}); });
@@ -937,12 +939,12 @@ describe("PlanningModeModal", () => {
}); });
describe("Summary markdown preview toggle", () => { describe("Summary markdown preview toggle", () => {
it("toggles description between plain textarea and formatted markdown preview", async () => { async function renderPlanningSummary(description: string) {
mockConnectPlanningStream.mockImplementationOnce((_sessionId: string, _projectId: string | undefined, handlers: any) => { mockConnectPlanningStream.mockImplementationOnce((_sessionId: string, _projectId: string | undefined, handlers: any) => {
setTimeout(() => { setTimeout(() => {
handlers.onSummary?.({ handlers.onSummary?.({
...mockSummary, ...mockSummary,
description: "## Heading\n\n- item\n\n**bold**", description,
}); });
}, 10); }, 10);
@@ -952,7 +954,7 @@ describe("PlanningModeModal", () => {
}; };
}); });
const { container } = render( const result = render(
<PlanningModeModal <PlanningModeModal
isOpen={true} isOpen={true}
onClose={mockOnClose} onClose={mockOnClose}
@@ -971,6 +973,12 @@ describe("PlanningModeModal", () => {
expect(screen.getByText("Planning Complete!")).toBeDefined(); expect(screen.getByText("Planning Complete!")).toBeDefined();
}); });
return result;
}
it("toggles description between plain textarea and formatted markdown preview", async () => {
const { container } = await renderPlanningSummary("## Heading\n\n- item\n\n**bold**");
expect(container.querySelector(".planning-textarea")).not.toBeNull(); expect(container.querySelector(".planning-textarea")).not.toBeNull();
expect(container.querySelector(".planning-description-preview")).toBeNull(); expect(container.querySelector(".planning-description-preview")).toBeNull();
@@ -988,6 +996,55 @@ describe("PlanningModeModal", () => {
expect(container.querySelector(".planning-textarea")).not.toBeNull(); expect(container.querySelector(".planning-textarea")).not.toBeNull();
}); });
}); });
it("expands and collapses the mobile summary description without breaking the adjacent markdown toggle", async () => {
mockViewport("mobile");
const { container } = await renderPlanningSummary(
"## Heading\n\n" +
"This is a long planning description that should visibly change between collapsed and expanded summary states on mobile.\n\n" +
"- Keep markdown available\n" +
"- Keep expand available",
);
const markdownToggle = screen.getByTestId("planning-description-markdown-toggle");
const expandButton = screen.getByRole("button", { name: "Expand description" });
expect(markdownToggle.closest("label")).toBeNull();
expect(expandButton.closest("label")).toBeNull();
const textarea = container.querySelector<HTMLTextAreaElement>(".planning-textarea");
expect(textarea).not.toBeNull();
expect(textarea?.classList.contains("expanded")).toBe(false);
fireEvent.click(expandButton);
await waitFor(() => {
expect(screen.getByRole("button", { name: "Collapse description" })).toBeDefined();
expect(container.querySelector(".planning-textarea")?.classList.contains("expanded")).toBe(true);
});
fireEvent.click(markdownToggle);
await waitFor(() => {
const preview = container.querySelector(".planning-description-preview");
expect(preview).not.toBeNull();
expect(preview?.classList.contains("expanded")).toBe(true);
expect(screen.getByRole("heading", { level: 2, name: "Heading" })).toBeDefined();
});
fireEvent.click(screen.getByRole("button", { name: "Collapse description" }));
await waitFor(() => {
expect(screen.getByRole("button", { name: "Expand description" })).toBeDefined();
expect(container.querySelector(".planning-description-preview")?.classList.contains("expanded")).toBe(false);
});
fireEvent.click(markdownToggle);
await waitFor(() => {
expect(container.querySelector(".planning-textarea")).not.toBeNull();
expect(container.querySelector(".planning-description-preview")).toBeNull();
});
});
}); });
}); });