feat(chrome): shared account menu and global app header

Move account actions (Settings, Downloads, Display preferences, Sign
out) out of the library-only header into a shared AccountMenu anchored in
a global AppHeader, available on every authenticated non-immersive
screen. Add a layoutShell helper deciding where chrome shows, expose
serverName/serverUrl auth stores, and a display view-mode preference. The
settings page also gains the UR-053 WiFi-only toggle.

TRACES: UR-054 | DR-075, DR-076, DR-077
This commit is contained in:
2026-07-23 20:03:12 +02:00
parent f25deba824
commit cf9472f04f
9 changed files with 643 additions and 19 deletions
+28
View File
@@ -18,6 +18,7 @@ import { describe, it, expect } from "vitest";
import {
showBottomNav,
showGlobalMiniPlayer,
showGlobalHeader,
routeOwnsLayout,
showBottomUi,
} from "./layoutShell";
@@ -87,6 +88,33 @@ describe("routeOwnsLayout", () => {
});
});
describe("showGlobalHeader", () => {
it("shows the root-owned header (with account menu) on home, search, downloads", () => {
// The whole point of UR-054: account actions reachable without going to Library.
expect(showGlobalHeader(authed("/"))).toBe(true);
expect(showGlobalHeader(authed("/search"))).toBe(true);
expect(showGlobalHeader(authed("/downloads"))).toBe(true);
});
it("does NOT show on library (it renders its own AppHeader)", () => {
expect(showGlobalHeader(authed("/library"))).toBe(false);
expect(showGlobalHeader(authed("/library/abc"))).toBe(false);
});
it("does NOT show on settings (owns its content; no account menu needed there)", () => {
expect(showGlobalHeader(authed("/settings"))).toBe(false);
});
it("does NOT show on the immersive player or login", () => {
expect(showGlobalHeader(authed("/player/x"))).toBe(false);
expect(showGlobalHeader(authed("/login"))).toBe(false);
});
it("hides when unauthenticated", () => {
expect(showGlobalHeader({ pathname: "/", isAuthenticated: false })).toBe(false);
});
});
describe("structural invariant: every route that shows bottom UI has a scroller above it", () => {
// With the in-flow model, "the bottom UI is a flex sibling below a scroller"
// must hold on every route where it shows. That scroller is provided by