FN-9124: Move mission planning CTA to top of list

Place the primary mission-planning action consistently above mission content across desktop and mobile layouts.

- move the Plan New Mission and Create actions above the mission list
- increase the mission CTA height with shared design tokens and remove duplicate empty-state controls
- document and test placement, sizing, create-mode, empty-state, and responsive behavior

Files changed:
 .changeset/fn-9124-mission-cta-top.md              |   7 ++
 docs/missions.md                                   |   2 +-
 .../dashboard/app/components/MissionManager.css    |  42 +++----
 .../dashboard/app/components/MissionManager.tsx    |  89 +++++++-------
 .../dashboard/app/components/PlanningModeModal.css |  14 +--
 .../dashboard/app/components/PlanningModeModal.tsx |  12 +-
 .../__tests__/MissionManager.mobile-css.test.ts    |  33 +++--
 .../MissionManager.plan-cta-placement.test.tsx     | 134 +++++++++++++++++++++
 8 files changed, 238 insertions(+), 95 deletions(-)

Fusion-Task-Id: FN-9124

Fusion-Task-Lineage: 09ad7695-80ed-46af-a1c1-f334952c37ec

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-08-16 08:05:11 -07:00
parent 53acaf96c3
commit 385059f5ca
8 changed files with 240 additions and 97 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Plan New Mission now sits at the top of the mission list and is slightly taller.
category: fix
dev: Replaced footer CTA wrappers with top mission-manager__sidebar-cta-bar and mission-list__header-actions containers using calc(var(--space-lg) * 2 + var(--space-sm)); removed the duplicate empty-state CTA.

View File

@@ -144,7 +144,7 @@ This keeps member execution isolated per task while still routing member landing
Use the Mission Manager UI to create missions and build hierarchy interactively.
On mobile, Mission Manager surfaces the primary **Plan New Mission** CTA at the top of the mission list for faster access, while desktop keeps the split-layout sidebar CTA anchored in the bottom action region as the primary entry point.
Mission Manager anchors the primary **Plan New Mission** CTA at the top of the mission list on both mobile stacked and desktop split layouts. The secondary **Create** manual-create link sits beneath it, and both primary controls use the same slightly taller tokenized height. While the inline create form is open, the CTA is suppressed cleanly without leaving an empty container; each surface retains only one primary planning control.
Mission detail refreshes now preserve expanded milestone/slice state and keep the selected milestone expanded, so persisted milestone acceptance criteria remain visible across live updates.

View File

@@ -225,19 +225,23 @@ Title metric matches the shared ViewHeader (1.125rem) so the Missions header rea
box-shadow: var(--focus-ring-strong);
}
/* Desktop split-only sidebar footer action bar for mission creation CTA. */
.mission-manager__sidebar-footer {
/*
FNXC:MissionsUI 2026-08-16-14:48:
The desktop CTA bar stays above the scrolling mission list so planning is immediately reachable.
*/
.mission-manager__sidebar-cta-bar {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--space-sm);
padding: var(--space-md);
border-top: var(--btn-border-width) solid var(--border);
border-bottom: var(--btn-border-width) solid var(--border);
}
.mission-manager__sidebar-cta {
width: 100%;
min-height: calc(var(--space-lg) * 2 + var(--space-xs));
min-height: calc(var(--space-lg) * 2 + var(--space-sm));
justify-content: center;
gap: var(--space-sm);
font-size: inherit;
@@ -388,11 +392,6 @@ primary CTA while every production presentation can open the merge-aware form.
color: var(--text-muted);
}
.mission-manager__empty-cta {
margin-top: var(--space-sm);
gap: var(--space-sm);
}
/* ── Status Badge ── */
.mission-status-badge {
display: inline-flex;
@@ -556,11 +555,12 @@ primary CTA while every production presentation can open the merge-aware form.
}
/*
FNXC:MissionsMobile 2026-06-22-18:00:
Narrow/mobile Missions puts Plan New Mission at the bottom of the list, using the same compact primary button proportions as Chat's New Chat action instead of a large top CTA.
FNXC:MissionsUI 2026-08-16-14:48:
Mobile Missions keeps Plan New Mission above the list for immediate reachability. The shared tokenized height matches the desktop sidebar CTA while preserving the compact button treatment.
*/
.mission-list__primary-cta {
width: 100%;
min-height: calc(var(--space-lg) * 2 + var(--space-sm));
justify-content: center;
gap: var(--space-sm);
}
@@ -580,12 +580,6 @@ Narrow/mobile Missions puts Plan New Mission at the bottom of the list, using th
font-weight: 600;
}
.mission-list__footer {
display: flex;
flex-direction: column;
gap: var(--space-sm);
}
.mission-list__resume-actions {
display: flex;
flex-wrap: wrap;
@@ -594,20 +588,20 @@ Narrow/mobile Missions puts Plan New Mission at the bottom of the list, using th
margin-inline-start: auto;
}
.mission-list__footer-actions {
.mission-list__header-actions {
display: flex;
flex-direction: column;
gap: var(--space-sm);
}
/* Desktop split-only: list footer actions live in sidebar header, so hide duplicated footer buttons. */
.mission-manager__sidebar .mission-list__footer-actions {
/*
FNXC:MissionsUI 2026-08-16-14:48:
Desktop owns the CTA in its sidebar bar, so the shared list header cannot duplicate it.
*/
.mission-manager__sidebar .mission-list__header-actions {
display: none;
}
.mission-list__footer-actions .mission-list__manual-create-link {
align-self: center;
}
/* ── Inline Form Card ── */
.mission-form-card {
display: flex;

View File

@@ -5293,14 +5293,30 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
);
});
const renderMissionListContent = ({ hideBottomButtons = false }: { hideBottomButtons?: boolean } = {}) => {
const renderMissionListContent = ({ hideListCta = false }: { hideListCta?: boolean } = {}) => {
const persistedInterviewMissions = missions.filter((mission) => mission.interviewState === "in_progress");
const standardMissions = missions.filter((mission) => mission.interviewState !== "in_progress");
const showBottomPlanButton = !hideBottomButtons;
const showListCta = !hideListCta;
return (
<div className="mission-list">
{/* Create mission form */}
{/*
FNXC:MissionsUI 2026-08-16-14:48:
Mobile Missions anchors its single Plan New Mission CTA above every list state so it is immediately reachable. The inline create form suppresses this whole container rather than leaving an empty header shell.
*/}
{!isCreatingMission && showListCta && (
<div className="mission-list__header-actions">
<button className="btn btn-sm btn-primary mission-list__primary-cta" onClick={openNewMissionInterview}>
<Sparkles size={14} />
{t("missions.planNewMission", "Plan New Mission")}
</button>
<a className="mission-list__manual-create-link" href="#mission-create" onClick={openDirectMissionCreate}>
{t("missions.createButton", "Create")}
</a>
</div>
)}
{/* Create mission form */}
{isCreatingMission && (
<div className="mission-form-card">
<input
@@ -5531,34 +5547,9 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
<p className="mission-manager__empty-body">
{t("missions.noMissionsYetBody", "Missions are large initiatives that bundle milestones, slices, and features into a single plan. Plan a mission to break down a goal end-to-end and let agents work through it autopilot-style.")}
</p>
<button
className="btn btn-sm btn-primary mission-manager__empty-cta"
onClick={openNewMissionInterview}
>
<Sparkles size={14} />
{t("missions.planNewMission", "Plan New Mission")}
</button>
<a className="mission-list__manual-create-link" href="#mission-create" onClick={openDirectMissionCreate}>
{t("missions.createButton", "Create")}
</a>
</div>
)}
{!isCreatingMission && (
<div className="mission-list__footer">
{showBottomPlanButton && (
<div className="mission-list__footer-actions">
<button className="btn btn-sm btn-primary mission-list__primary-cta" onClick={openNewMissionInterview}>
<Sparkles size={14} />
{t("missions.planNewMission", "Plan New Mission")}
</button>
<a className="mission-list__manual-create-link" href="#mission-create" onClick={openDirectMissionCreate}>
{t("missions.createButton", "Create")}
</a>
</div>
)}
</div>
)}
</div>
);
};
@@ -5707,6 +5698,31 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
aria-label={t("missions.missionList", "Mission list")}
style={isMobile ? undefined : { width: `${sidebarWidth}px` }}
>
{/*
FNXC:MissionsUI 2026-08-16-14:48:
Desktop Missions anchors its single Plan New Mission CTA before the scrolling list for immediate reachability. Empty and create-form states never duplicate the CTA, and create mode omits the bar unless the delete confirmation still needs it.
*/}
{(shouldRenderSidebarDeleteConfirm || !isCreatingMission) && (
<div className="mission-manager__sidebar-cta-bar" data-testid="mission-sidebar-cta-bar">
{shouldRenderSidebarDeleteConfirm && renderDeleteConfirmPanel()}
{!isCreatingMission && (
<>
<button
className="btn btn-primary mission-manager__sidebar-cta"
onClick={openNewMissionInterview}
title={t("missions.planNewMission", "Plan New Mission")}
aria-label={t("missions.planNewMission", "Plan New Mission")}
>
<Sparkles size={14} />
{t("missions.planNewMission", "Plan New Mission")}
</button>
<a className="mission-list__manual-create-link" href="#mission-create" onClick={openDirectMissionCreate}>
{t("missions.createButton", "Create")}
</a>
</>
)}
</div>
)}
<div className="mission-manager__sidebar-list">
{loading ? (
<div className="mission-manager__loading">
@@ -5714,24 +5730,9 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
<span>{t("missions.loadingMissions", "Loading missions...")}</span>
</div>
) : (
renderMissionListContent({ hideBottomButtons: true })
renderMissionListContent({ hideListCta: true })
)}
</div>
<div className="mission-manager__sidebar-footer" data-testid="mission-sidebar-footer">
{shouldRenderSidebarDeleteConfirm && renderDeleteConfirmPanel()}
<button
className="btn btn-primary mission-manager__sidebar-cta"
onClick={openNewMissionInterview}
title={t("missions.planNewMission", "Plan New Mission")}
aria-label={t("missions.planNewMission", "Plan New Mission")}
>
<Sparkles size={14} />
{t("missions.planNewMission", "Plan New Mission")}
</button>
<a className="mission-list__manual-create-link" href="#mission-create" onClick={openDirectMissionCreate}>
{t("missions.createButton", "Create")}
</a>
</div>
</aside>
{!isMobile && (

View File

@@ -53,7 +53,7 @@ FNXC:PlanningMode 2026-06-21-00:00:
FN-6886 promotes Planning Mode into the main app content area. The embedded shell fills the available content pane and intentionally disables modal-only sizing/resizing so the left sidebar owns navigation and no backdrop shell remains.
*/
/*
FNXC:Planning 2026-06-23-02:00:
FNXC:Planning 2026-08-16-14:48:
The embedded Planning wrapper matches Missions' shell: NO outer padding. Missions' inline shell (mission-manager--inline) has no wrapper inset — its header supplies the top/side padding (var(--space-lg) var(--space-xl)) and the split body fills edge-to-edge below. Mirroring that here removes the previous extra var(--space-lg) inset so Planning's header and two-pane body align with Missions.
*/
.planning-view {
@@ -293,7 +293,7 @@ The embedded planning header must be a visual SIBLING of MissionManager's inline
}
/*
FNXC:Planning 2026-06-23-01:15:
FNXC:Planning 2026-08-16-14:48:
Embedded Planning is a real two-pane view mirroring Missions (mission-manager__split): the left sidebar is a full-height flex column (list scrolls, footer pinned to bottom) and the right detail pane fills the remaining width AND height. The sidebar surface/border/width treatment matches mission-manager__sidebar — var(--surface) background, a single border-right divider — so the two views read consistently. The flex chain planning-view -> planning-modal--embedded -> planning-modal-body keeps the whole layout filling the main-content pane to the bottom.
*/
.planning-sidebar {
@@ -308,7 +308,7 @@ Embedded Planning is a real two-pane view mirroring Missions (mission-manager__s
}
/*
FNXC:Planning 2026-06-23-02:00:
FNXC:Planning 2026-08-16-14:48:
Sidebar resize handle — identical treatment to MissionManager's mission-manager__sidebar-resize-handle: a thin (var(--space-sm)) col-resize strip whose ::before center bar tints with var(--todo) on hover/active, plus a focus ring for keyboard resize. touch-action:none keeps the pointer drag from scrolling on trackpads.
*/
.planning-sidebar-resize-handle {
@@ -342,8 +342,8 @@ Sidebar resize handle — identical treatment to MissionManager's mission-manage
}
/*
FNXC:Planning 2026-06-23-02:00:
Footer matches MissionManager's mission-manager__sidebar-footer EXACTLY: padding var(--space-md), a single border-top divider (var(--btn-border-width) solid var(--border)), gap var(--space-sm), centered. Missions has no separate footer band styling beyond the centered CTA, so we mirror that — no extra bottom padding/margin and no empty space. The column direction is the only deviation, required because Planning carries the show/hide-archived link beneath the CTA (Missions has a single child); align-items stays stretch so the full-width CTA fills the footer like Missions'.
FNXC:Planning 2026-08-16-14:48:
Planning intentionally retains this bottom-pinned session footer at its current metrics. Missions now uses a top-anchored sidebar CTA, so this footer preserves its own padding, border-top, gap, and column layout for the archived-session link.
*/
.planning-sidebar-footer {
display: flex;
@@ -377,8 +377,8 @@ Footer matches MissionManager's mission-manager__sidebar-footer EXACTLY: padding
}
/*
FNXC:Planning 2026-06-23-02:00:
The New session button must look EXACTLY like Missions' primary sidebar create button (mission-manager__sidebar-cta with btn btn-primary): same full-width fill, same min-height (calc(--space-lg * 2 + --space-xs)), centered icon+label gap, inherited font-size. The earlier .active "inset ring" outline is REMOVED — Missions' CTA has no ring in any state, so the no-session-selected (.active) state now relies solely on the empty/initial detail pane to signal the new-session view, keeping the two buttons visually identical.
FNXC:Planning 2026-08-16-14:48:
Planning intentionally retains this bottom-pinned New session button at its current metrics. Missions now anchors its slightly taller CTA at the top of its sidebar; both controls reuse the shared primary button treatment without requiring exact height parity. The earlier .active "inset ring" outline is REMOVED so the no-session-selected (.active) state relies solely on the empty/initial detail pane.
*/
.planning-sidebar-new {
width: 100%;

View File

@@ -74,7 +74,7 @@ import { MicButton } from "./MicButton";
const WARNING_ICON = "⚠️";
/*
FNXC:Planning 2026-06-23-02:00:
FNXC:Planning 2026-08-16-14:48:
The embedded Planning sidebar is resizable exactly like Missions (MissionManager's MISSION_SIDEBAR_* constants). Default 300px matches Missions' default (calc(--space-lg 16px * 18.75)); min/max/storage mirror Missions so the two views resize identically and persist independently.
*/
const PLANNING_SIDEBAR_DEFAULT_WIDTH = 300;
@@ -957,7 +957,7 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
}, [isRefineMenuOpen]);
/*
FNXC:Planning 2026-06-23-02:00:
FNXC:Planning 2026-08-16-14:48:
Resizable Planning sidebar — pointer-drag + arrow-key resize with localStorage persistence, mirroring MissionManager.handleSidebarResizeStart/handleSidebarResizeKeyDown. Width is clamped to PLANNING_SIDEBAR_MIN/MAX and applied as an inline width on the sidebar <aside>. Disabled on mobile where the sidebar stacks full-width.
*/
const [sidebarWidth, setSidebarWidth] = useState<number>(() => {
@@ -5556,8 +5556,8 @@ function PlanningSessionList({
style={sidebarWidth === undefined ? undefined : { width: `${sidebarWidth}px` }}
>
{/*
FNXC:Planning 2026-06-23-01:15:
The embedded Planning view reads as a real two-pane layout matching Missions: the left sidebar is a full-height flex column whose session list scrolls and whose primary action ("New session") is pinned to a bottom footer (parity with MissionManager's mission-manager__sidebar-footer + sidebar-cta). The header that previously held the New session button is removed so the list owns the top of the sidebar like the Missions list.
FNXC:Planning 2026-08-16-14:48:
Planning intentionally keeps its primary "New session" action pinned to a bottom footer while its session list scrolls. Missions now anchors its slightly taller CTA at the top of its sidebar, so the two surfaces do not require footer-placement parity.
*/}
<div className="planning-sidebar-list">
{/*
@@ -5679,8 +5679,8 @@ function PlanningSessionList({
</div>
<div className="planning-sidebar-footer">
{/*
FNXC:Planning 2026-06-23-01:15:
The New session CTA mirrors Missions' primary sidebar action: it reuses the shared "btn btn-primary" look (same base button class MissionManager pairs with mission-manager__sidebar-cta) so size and color match the Missions create button exactly, full-width and bottom-anchored. The "active" state (no session selected) keeps a subtle accent so the user can tell they're on the new-session view.
FNXC:Planning 2026-08-16-14:48:
Planning keeps its bottom-anchored New session CTA at its current metrics. It shares the "btn btn-primary" treatment with Missions, whose slightly taller CTA now lives at the top of the mission sidebar; exact placement and height parity are intentionally not required.
*/}
<button
className={`btn btn-primary planning-sidebar-new ${selectedSessionId === null ? "active" : ""}`}

View File

@@ -106,15 +106,14 @@ describe("MissionManager mobile styles", () => {
expect(section).toContain("display: block;");
});
it("keeps the mobile bottom mission CTA full-width and primary-styled", () => {
it("keeps the mobile top mission CTA full-width, primary-styled, and tokenized", () => {
const css = loadAllAppCss();
const ctaRule = css.match(/\.mission-list__primary-cta\s*\{[^}]*\}/)?.[0];
expect(css).not.toContain(".mission-list__top-action");
const topCtaRule = css.match(/\.mission-list__primary-cta\s*\{[^}]*\}/)?.[0];
expect(topCtaRule).toContain("width: 100%;");
expect(topCtaRule).toContain("justify-content: center;");
expect(topCtaRule).toContain("gap: var(--space-sm);");
expect(ctaRule).toContain("width: 100%;");
expect(ctaRule).toContain("min-height: calc(var(--space-lg) * 2 + var(--space-sm));");
expect(ctaRule).toContain("justify-content: center;");
expect(ctaRule).toContain("gap: var(--space-sm);");
});
it("hides back button on desktop and restores it on mobile", () => {
@@ -177,16 +176,24 @@ describe("desktop two-panel split CSS", () => {
expect(css).toContain("overflow-y: auto;");
});
it("anchors desktop Plan New Mission CTA in a sidebar footer with tokenized height", () => {
it("anchors desktop Plan New Mission CTA above the list with the shared tokenized height", () => {
const css = loadAllAppCss();
const barRule = css.match(/\.mission-manager__sidebar-cta-bar\s*\{[^}]*\}/)?.[0];
const ctaRule = css.match(/\.mission-manager__sidebar-cta\s*\{[^}]*\}/)?.[0];
expect(barRule).toContain("border-bottom: var(--btn-border-width) solid var(--border);");
expect(ctaRule).toContain("width: 100%;");
expect(ctaRule).toContain("min-height: calc(var(--space-lg) * 2 + var(--space-sm));");
expect(ctaRule).toContain("justify-content: center;");
});
it("removes obsolete mission CTA wrappers and empty-state styling", () => {
const css = loadAllAppCss();
const footerRule = css.match(/\.mission-manager__sidebar-footer\s*\{[^}]*\}/)?.[0];
expect(footerRule).toContain("border-top: var(--btn-border-width) solid var(--border);");
const ctaRule = css.match(/\.mission-manager__sidebar-cta\s*\{[^}]*\}/)?.[0];
expect(ctaRule).toContain("width: 100%;");
expect(ctaRule).toContain("min-height: calc(var(--space-lg) * 2 + var(--space-xs));");
expect(ctaRule).toContain("justify-content: center;");
expect(css).not.toContain(".mission-manager__sidebar-footer");
expect(css).not.toContain(".mission-list__footer");
expect(css).not.toContain(".mission-list__footer-actions");
expect(css).not.toContain(".mission-manager__empty-cta");
});
it("defines split container as flex row", () => {

View File

@@ -0,0 +1,134 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { MissionManager } from "../MissionManager";
const mockFetchMissions = vi.fn();
const mockFetchMissionsHealth = vi.fn();
const mockFetchAiSessions = vi.fn();
const mockFetchMissionInterviewDrafts = vi.fn();
vi.mock("../../hooks/useNavigationHistory", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../hooks/useNavigationHistory")>();
return { ...actual, useNavigationHistoryContext: () => ({ pushNav: vi.fn(), replaceCurrent: vi.fn() }) };
});
vi.mock("../../api", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../api")>();
return {
...actual,
fetchMissions: (...args: unknown[]) => mockFetchMissions(...args),
fetchMissionsHealth: (...args: unknown[]) => mockFetchMissionsHealth(...args),
fetchAiSessions: (...args: unknown[]) => mockFetchAiSessions(...args),
fetchMissionInterviewDrafts: (...args: unknown[]) => mockFetchMissionInterviewDrafts(...args),
};
});
const now = "2026-08-16T14:48:00.000Z";
const leftoverShells = ".mission-manager__sidebar-footer, .mission-list__footer, .mission-list__footer-actions, [data-testid='mission-sidebar-footer']";
function mission() {
return { id: "M-001", title: "Top CTA Mission", description: "", status: "planning", milestones: [], createdAt: now, updatedAt: now };
}
function setViewport({ width, mobile = false }: { width: number; mobile?: boolean }) {
Object.defineProperty(window, "innerWidth", { value: width, configurable: true });
Object.defineProperty(window, "matchMedia", {
configurable: true,
value: vi.fn().mockImplementation((query: string) => ({
matches: mobile && query.includes("max-width"), media: query, onchange: null,
addListener: vi.fn(), removeListener: vi.fn(), addEventListener: vi.fn(), removeEventListener: vi.fn(), dispatchEvent: vi.fn(),
})),
});
}
function renderManager() {
return render(<MissionManager isInline isOpen onClose={() => {}} addToast={() => {}} projectId="project-1" />);
}
beforeEach(() => {
vi.clearAllMocks();
localStorage.clear();
mockFetchMissionsHealth.mockResolvedValue({});
mockFetchAiSessions.mockResolvedValue([]);
mockFetchMissionInterviewDrafts.mockResolvedValue([]);
});
describe("MissionManager Plan New Mission placement", () => {
it("places the desktop CTA before a populated scrolling sidebar list and opens the interview", async () => {
setViewport({ width: 1440 });
mockFetchMissions.mockResolvedValue([mission()]);
renderManager();
await screen.findByText("Top CTA Mission");
const sidebar = screen.getByTestId("mission-sidebar");
const ctaBar = sidebar.querySelector(".mission-manager__sidebar-cta-bar");
const list = sidebar.querySelector(".mission-manager__sidebar-list");
expect(ctaBar?.compareDocumentPosition(list!)).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
expect(document.querySelector(leftoverShells)).toBeNull();
fireEvent.click(screen.getByRole("button", { name: /Plan New Mission/i, hidden: true }));
await waitFor(() => expect(screen.getByRole("dialog")).toBeInTheDocument());
});
it("places the mobile CTA before populated mission items without footer wrappers", async () => {
setViewport({ width: 390, mobile: true });
mockFetchMissions.mockResolvedValue([mission()]);
renderManager();
await screen.findByText("Top CTA Mission");
const list = document.querySelector(".mission-list")!;
expect(list.firstElementChild).toHaveClass("mission-list__header-actions");
expect(list.querySelector(".mission-list__header-actions")?.compareDocumentPosition(list.querySelector(".mission-list__item")!)).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
expect(document.querySelector(leftoverShells)).toBeNull();
});
it("renders one desktop empty-state CTA with no duplicate empty-state control", async () => {
setViewport({ width: 1440 });
mockFetchMissions.mockResolvedValue([]);
renderManager();
await screen.findByText("No missions yet");
expect(screen.getAllByRole("button", { name: /Plan New Mission/i, hidden: true })).toHaveLength(1);
expect(document.querySelector(".mission-manager__empty-cta")).toBeNull();
expect(document.querySelector(leftoverShells)).toBeNull();
});
it("renders one mobile empty-state CTA with no duplicate empty-state control", async () => {
setViewport({ width: 390, mobile: true });
mockFetchMissions.mockResolvedValue([]);
renderManager();
await screen.findByText("No missions yet");
expect(screen.getAllByRole("button", { name: /Plan New Mission/i, hidden: true })).toHaveLength(1);
expect(document.querySelector(".mission-manager__empty-cta")).toBeNull();
expect(document.querySelector(leftoverShells)).toBeNull();
});
it("suppresses the mobile CTA header cleanly while creating", async () => {
setViewport({ width: 390, mobile: true });
mockFetchMissions.mockResolvedValue([mission()]);
renderManager();
await screen.findByText("Top CTA Mission");
fireEvent.click(document.querySelector<HTMLAnchorElement>(".mission-list__manual-create-link")!);
await screen.findByLabelText("Mission auto-merge override");
expect(screen.queryAllByRole("button", { name: /Plan New Mission/i, hidden: true })).toHaveLength(0);
expect(document.querySelector(".mission-list__header-actions")).toBeNull();
expect(document.querySelector(leftoverShells)).toBeNull();
});
it("suppresses the desktop CTA bar cleanly while creating and retains the list", async () => {
setViewport({ width: 1440 });
mockFetchMissions.mockResolvedValue([mission()]);
renderManager();
await screen.findByText("Top CTA Mission");
fireEvent.click(document.querySelector<HTMLAnchorElement>(".mission-list__manual-create-link")!);
await screen.findByLabelText("Mission auto-merge override");
expect(screen.queryAllByRole("button", { name: /Plan New Mission/i, hidden: true })).toHaveLength(0);
expect(document.querySelector(".mission-manager__sidebar-cta-bar")).toBeNull();
expect(document.querySelector(".mission-manager__sidebar-list")).toBeInTheDocument();
expect(document.querySelector(leftoverShells)).toBeNull();
});
});