fix(profiles): keep the picker chrome-free

/profiles was unknown to the layout shell, so reaching it from Settings
rendered the bottom nav and global header over the picker -- letting
someone tab straight past the profile they were being asked to choose,
and offering an in-app mini player as a route back into the previous
profile's queue.

Grouped with /login throughout layoutShell: it is a gate, not a page. OS
lockscreen transport controls are unaffected, which is what keeps playing
audio controllable while the app is locked.
This commit is contained in:
2026-08-30 19:30:51 +02:00
parent da762da55d
commit fb3d0014ef
2 changed files with 51 additions and 5 deletions
+34
View File
@@ -202,3 +202,37 @@ describe("showHeaderSearch", () => {
expect(showHeaderSearch({ pathname: "/settings" })).toBe(false);
});
});
/**
* The profile picker is a gate, not a page. It must be chrome-free for the same
* reason /login is: bottom nav over a "who's watching" screen lets someone tab
* straight past the profile they were being asked to choose, and an in-app mini
* player on it would offer a route into the previous profile's queue. OS
* lockscreen transport controls are unaffected — those are what keep playing
* audio controllable while the app is locked (DR-275).
*
* TRACES: UR-082 | DR-276
*/
describe("profile picker chrome", () => {
const pathname = "/profiles";
it("shows no bottom nav, even authenticated", () => {
expect(showBottomNav({ pathname, isAuthenticated: true })).toBe(false);
});
it("shows no global mini player", () => {
expect(showGlobalMiniPlayer({ pathname })).toBe(false);
});
it("shows no global header", () => {
expect(showGlobalHeader({ pathname, isAuthenticated: true })).toBe(false);
});
it("owns its own layout", () => {
expect(routeOwnsLayout({ pathname })).toBe(true);
});
it("lets the shell reserve the bottom inset, since nothing else will", () => {
expect(shellReservesBottomInset({ pathname, isAuthenticated: true })).toBe(true);
});
});
+17 -5
View File
@@ -28,10 +28,19 @@ export interface BottomUiVisibilityInput {
/**
* The bottom nav is shown on every authenticated route except the full-screen
* player and the login route.
* player, the login route, and the profile picker.
*
* `/profiles` is grouped with `/login` throughout this module because it is a
* gate rather than a page: a nav bar over "who's watching" lets someone tab
* straight past the choice they were being asked to make.
*/
export function showBottomNav({ pathname, isAuthenticated }: BottomUiVisibilityInput): boolean {
return isAuthenticated && !pathname.startsWith("/player/") && !pathname.startsWith("/login");
return (
isAuthenticated &&
!pathname.startsWith("/player/") &&
!pathname.startsWith("/login") &&
!pathname.startsWith("/profiles")
);
}
/**
@@ -44,6 +53,7 @@ export function showGlobalMiniPlayer({ pathname }: { pathname: string }): boolea
return (
!pathname.startsWith("/player/") &&
!pathname.startsWith("/login") &&
!pathname.startsWith("/profiles") &&
!pathname.startsWith("/settings")
);
}
@@ -58,7 +68,8 @@ export function routeOwnsLayout({ pathname }: { pathname: string }): boolean {
return (
pathname.startsWith("/library") ||
pathname.startsWith("/player/") ||
pathname.startsWith("/login")
pathname.startsWith("/login") ||
pathname.startsWith("/profiles")
);
}
@@ -69,7 +80,7 @@ export function routeOwnsLayout({ pathname }: { pathname: string }): boolean {
* 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`,
* immersive/chrome-free, as is `/profiles`. Everything else authenticated (`/`, `/search`,
* `/downloads`) gets the header from the root — the whole point of UR-054.
*
* TRACES: UR-054 | DR-076
@@ -109,7 +120,8 @@ export function showBottomUi(input: BottomUiVisibilityInput): boolean {
* Exactly one element may reserve it. BottomUi owns it whenever it renders,
* because the padding belongs *inside* its surface box so the colour extends
* behind the bar rather than leaving a strip of page background. On routes with
* no bottom UI at all (login, the full-screen player) nothing else would, so
* no bottom UI at all (login, the profile picker, the full-screen player)
* nothing else would, so
* the shell takes it.
*
* TRACES: UR-066 | DR-112