feat(FN-2398): restore compact project switch in tablet header

- Render the compact project switch trigger for tablet layouts by gating on compact mode instead of mobile-only mode
- Extend ProjectSelector compact-switch styles to the tablet media range so the trigger and dropdown render correctly beside the logo
- Update tablet header control regression coverage to expect the compact switch affordance and absence of the desktop selector trigger
- Add Header tablet interaction tests covering Escape key, outside click, and project selection closing behavior for the compact switch dropdown
This commit is contained in:
Fusion
2026-04-24 21:42:07 -07:00
committed by gsxdsm
parent 20366ee250
commit 2ef472f551
4 changed files with 73 additions and 9 deletions

View File

@@ -392,7 +392,7 @@ describe("tablet header controls", () => {
expect(screen.queryByPlaceholderText("Search tasks...")).toBeNull();
});
// ── Project selector hidden on tablet ──────────────────────────
// ── Tablet project-switch affordances ──────────────────────────
it("does not render project selector on tablet", () => {
const projects = [
@@ -434,7 +434,7 @@ describe("tablet header controls", () => {
expect(btn.textContent).toContain("Projects");
});
it("does not render mobile project switch trigger on tablet", () => {
it("renders compact project switch trigger beside logo on tablet", () => {
const projects = [
{ id: "1", name: "Project One", path: "/path/one", status: "active" as const },
{ id: "2", name: "Project Two", path: "/path/two", status: "active" as const },
@@ -444,7 +444,8 @@ describe("tablet header controls", () => {
currentProject: projects[0],
onSelectProject: vi.fn(),
});
expect(screen.queryByTestId("mobile-project-switch-trigger")).toBeNull();
expect(screen.getByTestId("mobile-project-switch-trigger")).toBeDefined();
expect(screen.queryByTestId("project-selector-trigger")).toBeNull();
});
// ── Desktop still shows everything inline ──────────────────────

View File

@@ -526,8 +526,8 @@ export function Header({
<h1 className="logo">Fusion</h1>
</div>
{/* Mobile Project Switch - dropdown trigger next to logo when 2+ projects (mobile only) */}
{isMobile && projects.length > 1 && onSelectProject && (
{/* Compact Project Switch - dropdown trigger next to logo when 2+ projects (mobile + tablet) */}
{isCompact && projects.length > 1 && onSelectProject && (
<div className="mobile-project-switch" ref={mobileProjectSwitchRef}>
<button
className={`mobile-project-switch-trigger${isMobileProjectSwitchOpen ? " mobile-project-switch-trigger--open" : ""}`}

View File

@@ -273,8 +273,8 @@
color: var(--todo);
}
/* Mobile styles for mobile-project-switch */
@media (max-width: 768px) {
/* Compact styles for mobile-project-switch (mobile + tablet) */
@media (max-width: 768px), (min-width: 769px) and (max-width: 1024px) {
.mobile-project-switch {
display: inline-flex;
align-items: center;

View File

@@ -972,7 +972,7 @@ describe("Header", () => {
expect(screen.queryByTestId("mobile-project-switch-trigger")).toBeNull();
});
it("does not render mobile project switch trigger on tablet", () => {
it("renders mobile project switch trigger on tablet with 2+ projects", () => {
const projects = [
{ id: "1", name: "Project One", path: "/path/one", status: "active" as const },
{ id: "2", name: "Project Two", path: "/path/two", status: "active" as const },
@@ -982,7 +982,7 @@ describe("Header", () => {
currentProject: projects[0],
onSelectProject: vi.fn(),
}, "tablet");
expect(screen.queryByTestId("mobile-project-switch-trigger")).toBeNull();
expect(screen.getByTestId("mobile-project-switch-trigger")).toBeDefined();
});
it("renders mobile project switch trigger on mobile with 2+ projects", () => {
@@ -1009,6 +1009,69 @@ describe("Header", () => {
}, "mobile");
expect(screen.queryByTestId("mobile-project-switch-trigger")).toBeNull();
});
it("closes compact project switch dropdown on Escape in tablet mode", async () => {
const projects = [
{ id: "1", name: "Project One", path: "/path/one", status: "active" as const },
{ id: "2", name: "Project Two", path: "/path/two", status: "paused" as const },
];
renderHeader({
projects,
currentProject: projects[0],
onSelectProject: vi.fn(),
}, "tablet");
fireEvent.click(screen.getByTestId("mobile-project-switch-trigger"));
expect(screen.getByTestId("mobile-project-switch-dropdown")).toBeDefined();
fireEvent.keyDown(document, { key: "Escape" });
await waitFor(() => {
expect(screen.queryByTestId("mobile-project-switch-dropdown")).toBeNull();
});
});
it("closes compact project switch dropdown on outside click in tablet mode", async () => {
const projects = [
{ id: "1", name: "Project One", path: "/path/one", status: "active" as const },
{ id: "2", name: "Project Two", path: "/path/two", status: "paused" as const },
];
renderHeader({
projects,
currentProject: projects[0],
onSelectProject: vi.fn(),
}, "tablet");
fireEvent.click(screen.getByTestId("mobile-project-switch-trigger"));
expect(screen.getByTestId("mobile-project-switch-dropdown")).toBeDefined();
fireEvent.mouseDown(document.body);
await waitFor(() => {
expect(screen.queryByTestId("mobile-project-switch-dropdown")).toBeNull();
});
});
it("closes compact project switch dropdown after selecting a project in tablet mode", async () => {
const projects = [
{ id: "1", name: "Project One", path: "/path/one", status: "active" as const },
{ id: "2", name: "Project Two", path: "/path/two", status: "paused" as const },
];
const onSelectProject = vi.fn();
renderHeader({
projects,
currentProject: projects[0],
onSelectProject,
}, "tablet");
fireEvent.click(screen.getByTestId("mobile-project-switch-trigger"));
fireEvent.click(screen.getByTestId("mobile-project-switch-item-2"));
expect(onSelectProject).toHaveBeenCalledWith(projects[1]);
await waitFor(() => {
expect(screen.queryByTestId("mobile-project-switch-dropdown")).toBeNull();
});
});
});
describe("Manage Projects action", () => {