refactor(frontend): each page fills the floating header itself; the header knows no page

The header used to pick its pills by `page === …` and import five module controls, and Plex's
Discover/Browse toggle needed a context bridge to reach the sub-view that lives in PlexBrowse.
Now the header is an empty band: PageShell hands its root box out as a slot, a page portals its
own pills into it (PageHeader), and the palette's Jump-to pill shows where no page does
(PageHeaderDefault, rendered inside the page's Suspense so a loading page shows no stand-in).
Plex's toggles take the sub-view as a prop; PlexViewBridge and its context are gone, and with
them the header's five layer suppressions.

No visible change: every header pill's box measured identical against the previous build on eight
pages, in Hungarian and English.
This commit is contained in:
2026-10-23 00:23:38 +02:00
parent 0b24f2d8fa
commit 4ef963b005
36 changed files with 530 additions and 316 deletions
@@ -0,0 +1,104 @@
// @vitest-environment jsdom
//
// The floating header knows no page: a page fills it through PageHeader (a portal into the header's
// root box), and the shell's PageHeaderDefault shows only while no page does. These pin the slot's
// three promises: the page's pills land IN the header, the default steps aside for them and comes
// back when the next page has none, and a shell without a header (the channel page) renders the
// page's pills nowhere.
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { render, cleanup, screen } from "@testing-library/react";
import PageShell, { PageHeader, PageHeaderDefault } from "./PageShell";
vi.mock("./PageScroller", () => ({
default: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}));
beforeEach(() => {
vi.stubGlobal(
"ResizeObserver",
class {
observe() {}
disconnect() {}
},
);
});
afterEach(() => {
cleanup();
vi.unstubAllGlobals();
});
const hdr = <div data-testid="hdr" />;
const fallback = (
<PageHeaderDefault>
<span>default</span>
</PageHeaderDefault>
);
describe("the header slot", () => {
it("puts a page's pills inside the header box", () => {
render(
<PageShell header={hdr}>
<PageHeader>
<span>search</span>
</PageHeader>
</PageShell>,
);
expect(screen.getByTestId("hdr").textContent).toBe("search");
});
it("shows the default on a page with no header of its own", () => {
render(<PageShell header={hdr}>{fallback}</PageShell>);
expect(screen.getByTestId("hdr").textContent).toBe("default");
});
it("hides the default while a page fills the header, and brings it back on the next page", () => {
const page = (own: boolean) => (
<PageShell header={hdr}>
{own && (
<PageHeader>
<span>search</span>
</PageHeader>
)}
{fallback}
</PageShell>
);
const { rerender } = render(page(true));
expect(screen.getByTestId("hdr").textContent).toBe("search");
rerender(page(false));
expect(screen.getByTestId("hdr").textContent).toBe("default");
rerender(page(true));
expect(screen.getByTestId("hdr").textContent).toBe("search");
});
it("keeps the default hidden while one of two PageHeaders on a page remains", () => {
const page = (both: boolean) => (
<PageShell header={hdr}>
<PageHeader>
<span>search</span>
</PageHeader>
{both && (
<PageHeader>
<span>toggle</span>
</PageHeader>
)}
{fallback}
</PageShell>
);
const { rerender } = render(page(true));
rerender(page(false));
expect(screen.getByTestId("hdr").textContent).toBe("search");
});
it("renders a page's pills nowhere in a shell without a header", () => {
render(
<PageShell>
<PageHeader>
<span>search</span>
</PageHeader>
{fallback}
</PageShell>,
);
expect(screen.queryByText("search")).toBeNull();
expect(screen.queryByText("default")).toBeNull();
});
});