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:
@@ -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
|
||||
|
||||
@@ -67,6 +67,29 @@ export function routeOwnsLayout({ pathname }: { pathname: string }): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the root layout should render the shared app header (logo, desktop
|
||||
* nav, and the account menu) for this route.
|
||||
*
|
||||
* Routes that own their layout (library) render their own AppHeader, so the
|
||||
* root must not double it up. `/settings` owns its content but deliberately has
|
||||
* no account menu (the user is already there). `/player/*` and `/login` are
|
||||
* immersive/chrome-free. Everything else authenticated (`/`, `/search`,
|
||||
* `/downloads`) gets the header from the root — the whole point of UR-054.
|
||||
*
|
||||
* TRACES: UR-054 | DR-076
|
||||
*/
|
||||
export function showGlobalHeader({
|
||||
pathname,
|
||||
isAuthenticated,
|
||||
}: BottomUiVisibilityInput): boolean {
|
||||
return (
|
||||
isAuthenticated &&
|
||||
!routeOwnsLayout({ pathname }) &&
|
||||
!pathname.startsWith("/settings")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether any bottom UI is showing for this route (mini player, nav, or both).
|
||||
* The bottom UI is rendered in flex flow below the scroller (see BottomUi.svelte),
|
||||
|
||||
Reference in New Issue
Block a user