feat(FN-707): hide project selector on mobile and add to overflow menu

- Hide ProjectSelector on mobile via hidden className and CSS media query
- Add project selector as menu item in mobile overflow menu
- Add CSS safety net (max-width: 767px) to ensure selector stays hidden
- Add comprehensive tests for mobile project selector visibility behavior
This commit is contained in:
gsxdsm
2026-04-02 13:53:00 -07:00
parent 5ee0903a0a
commit 974ffc2ef6
3 changed files with 110 additions and 4 deletions

View File

@@ -171,8 +171,8 @@ export function Header({
<img src="/logo.svg" alt="Fusion logo" className="header-logo" width={24} height={24} />
<h1 className="logo">Fusion</h1>
{/* Project Selector - shown when 2+ projects, placed next to logo/title */}
{projects.length > 1 && (
{/* Project Selector - shown when 2+ projects on desktop only */}
{!isMobile && projects.length > 1 && (
<div className="header-project-selector">
<ProjectSelector
projects={projects}
@@ -185,8 +185,8 @@ export function Header({
</div>
)}
{/* Back to All Projects button when viewing a specific project */}
{currentProject && onViewAllProjects && (
{/* Back to All Projects button when viewing a specific project (desktop only) */}
{!isMobile && currentProject && onViewAllProjects && (
<button
className="header-back-button"
onClick={onViewAllProjects}
@@ -468,6 +468,18 @@ export function Header({
role="menu"
aria-label="Additional header actions"
>
{/* Switch Project - in overflow on mobile */}
{projects.length > 1 && onViewAllProjects && (
<button
className="mobile-overflow-item"
onClick={() => handleOverflowAction(onViewAllProjects)}
role="menuitem"
data-testid="overflow-project-selector-btn"
>
<Folder size={16} />
<span>{currentProject ? currentProject.name : "Switch Project"}</span>
</button>
)}
{/* Files - in overflow on mobile */}
{onOpenFiles && (
<button

View File

@@ -544,6 +544,94 @@ describe("Header", () => {
expect(onSearchChange).toHaveBeenCalledWith("test");
});
it("does not render project selector trigger on mobile", () => {
const projects = [
{ id: "proj_1", name: "Project One", path: "/path/1", status: "active" as const, isolationMode: "in-process" as const, createdAt: "", updatedAt: "" },
{ id: "proj_2", name: "Project Two", path: "/path/2", status: "active" as const, isolationMode: "in-process" as const, createdAt: "", updatedAt: "" },
];
render(
<Header
projects={projects}
currentProject={projects[0]}
onSelectProject={vi.fn()}
onViewAllProjects={vi.fn()}
/>
);
expect(screen.queryByTestId("project-selector-trigger")).toBeNull();
});
it("does not render back-to-projects button on mobile", () => {
const projects = [
{ id: "proj_1", name: "Project One", path: "/path/1", status: "active" as const, isolationMode: "in-process" as const, createdAt: "", updatedAt: "" },
{ id: "proj_2", name: "Project Two", path: "/path/2", status: "active" as const, isolationMode: "in-process" as const, createdAt: "", updatedAt: "" },
];
render(
<Header
projects={projects}
currentProject={projects[0]}
onViewAllProjects={vi.fn()}
/>
);
expect(screen.queryByTestId("back-to-projects-btn")).toBeNull();
});
it("shows switch project item in overflow menu on mobile when multiple projects", () => {
const projects = [
{ id: "proj_1", name: "Project One", path: "/path/1", status: "active" as const, isolationMode: "in-process" as const, createdAt: "", updatedAt: "" },
{ id: "proj_2", name: "Project Two", path: "/path/2", status: "active" as const, isolationMode: "in-process" as const, createdAt: "", updatedAt: "" },
];
render(
<Header
projects={projects}
currentProject={projects[0]}
onSelectProject={vi.fn()}
onViewAllProjects={vi.fn()}
onOpenSettings={vi.fn()}
/>
);
fireEvent.click(screen.getByTitle("More header actions"));
const btn = screen.getByTestId("overflow-project-selector-btn");
expect(btn).toBeDefined();
// Should show the current project name
expect(btn.textContent).toContain("Project One");
});
it("overflow project selector calls onViewAllProjects when clicked", () => {
const projects = [
{ id: "proj_1", name: "Project One", path: "/path/1", status: "active" as const, isolationMode: "in-process" as const, createdAt: "", updatedAt: "" },
{ id: "proj_2", name: "Project Two", path: "/path/2", status: "active" as const, isolationMode: "in-process" as const, createdAt: "", updatedAt: "" },
];
const onViewAllProjects = vi.fn();
render(
<Header
projects={projects}
currentProject={projects[0]}
onSelectProject={vi.fn()}
onViewAllProjects={onViewAllProjects}
onOpenSettings={vi.fn()}
/>
);
fireEvent.click(screen.getByTitle("More header actions"));
fireEvent.click(screen.getByTestId("overflow-project-selector-btn"));
expect(onViewAllProjects).toHaveBeenCalledOnce();
});
it("does not show switch project in overflow menu with single project", () => {
const projects = [
{ id: "proj_1", name: "Project One", path: "/path/1", status: "active" as const, isolationMode: "in-process" as const, createdAt: "", updatedAt: "" },
];
render(
<Header
projects={projects}
currentProject={projects[0]}
onViewAllProjects={vi.fn()}
onOpenSettings={vi.fn()}
/>
);
fireEvent.click(screen.getByTitle("More header actions"));
expect(screen.queryByTestId("overflow-project-selector-btn")).toBeNull();
});
it("shows agents button in overflow menu on mobile when onOpenAgents provided", () => {
render(<Header onOpenSettings={vi.fn()} onOpenAgents={vi.fn()} />);
fireEvent.click(screen.getByTitle("More header actions"));

View File

@@ -4439,6 +4439,12 @@ body {
padding: 12px 12px;
}
/* Hide project selector and back button on mobile (belt-and-suspenders with conditional rendering) */
.header-project-selector,
.header-back-button {
display: none;
}
/* Mobile header: collapsible search and overflow menu */
.mobile-search-trigger {
display: flex;