Formatting was configured but never enforced: `bun run format:check` reported 199 unformatted files and ran in no workflow and in no git hook, so .prettierrc (printWidth 100, trailing commas) described an intention rather than the tree. This is the one-time sweep that makes the check gateable. Whitespace and token-reflow only -- no behavioural change: `bun run check` reports 0 errors and all 1053 frontend tests pass before and after. Kept out of every other commit on purpose. A 199-file diff mixed with real changes is unreviewable, and the next commit turns format:check into a hard CI gate so this cannot silently accumulate again.
120 lines
4.7 KiB
TypeScript
120 lines
4.7 KiB
TypeScript
/**
|
|
* Pure layout-shell visibility rules for the app's bottom UI (mini player
|
|
* stacked over the bottom nav).
|
|
*
|
|
* These rules used to live as inline `$derived` booleans scattered across the
|
|
* root and library `+layout.svelte` files and diverged per platform/route.
|
|
*
|
|
* The overlap bug ("last row hidden behind the nav") is now solved
|
|
* STRUCTURALLY, not by these rules: the bottom UI is rendered as an in-flow
|
|
* flex child below the scroller (see BottomUi.svelte), so the scroller is
|
|
* physically bounded above it and can never render behind it. There is no
|
|
* measurement and no reserved padding. These functions only decide *whether*
|
|
* each piece is visible on a given route.
|
|
*
|
|
* Keeping them pure makes the visibility contract unit-testable.
|
|
*
|
|
* TRACES: UR-005 | DR-009
|
|
*/
|
|
|
|
import { isSearchRoute } from "$lib/utils/searchScope";
|
|
|
|
export interface BottomUiVisibilityInput {
|
|
/** Current route pathname, e.g. `$page.url.pathname`. */
|
|
pathname: string;
|
|
/** Whether the user is authenticated. */
|
|
isAuthenticated: boolean;
|
|
}
|
|
|
|
/**
|
|
* The bottom nav is shown on every authenticated route except the full-screen
|
|
* player and the login route.
|
|
*/
|
|
export function showBottomNav({ pathname, isAuthenticated }: BottomUiVisibilityInput): boolean {
|
|
return isAuthenticated && !pathname.startsWith("/player/") && !pathname.startsWith("/login");
|
|
}
|
|
|
|
/**
|
|
* The global (root-owned) mini player is shown on every route except the
|
|
* full-screen player, login, and settings. Crucially this is NOT gated on
|
|
* platform or on `/library` — the root owns the mini player everywhere, so the
|
|
* library route must never render its own second one.
|
|
*/
|
|
export function showGlobalMiniPlayer({ pathname }: { pathname: string }): boolean {
|
|
return (
|
|
!pathname.startsWith("/player/") &&
|
|
!pathname.startsWith("/login") &&
|
|
!pathname.startsWith("/settings")
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Routes that render their own full-height flex column (header + scroller +
|
|
* their own in-flow BottomUi). The root leaves these as a plain clipped box and
|
|
* does not render its own BottomUi. Every other route renders into the root's
|
|
* scroller, with the root's in-flow BottomUi as a flex sibling below it.
|
|
*/
|
|
export function routeOwnsLayout({ pathname }: { pathname: string }): boolean {
|
|
return (
|
|
pathname.startsWith("/library") ||
|
|
pathname.startsWith("/player/") ||
|
|
pathname.startsWith("/login")
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 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 the header renders its search box on this route (md+ only; below md
|
|
* the bottom-nav Search tab and /search's own input serve that role).
|
|
*
|
|
* `/search` is included deliberately: the bar is the single md+ search input,
|
|
* so it must survive the hop onto the results page instead of being replaced by
|
|
* a second input belonging to that page. The library routes keep it because
|
|
* that is where a search is most often started.
|
|
*
|
|
* TRACES: UR-049, UR-054 | DR-063
|
|
*/
|
|
export function showHeaderSearch({ pathname }: { pathname: string }): boolean {
|
|
return pathname.startsWith("/library") || isSearchRoute(pathname);
|
|
}
|
|
|
|
/**
|
|
* 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),
|
|
* so this is purely a visibility question — there is no padding to reserve.
|
|
*/
|
|
export function showBottomUi(input: BottomUiVisibilityInput): boolean {
|
|
return showBottomNav(input) || showGlobalMiniPlayer({ pathname: input.pathname });
|
|
}
|
|
|
|
/**
|
|
* Whether the app shell itself must reserve the bottom safe-area inset
|
|
* (`--safe-bottom`, i.e. the Android navigation/gesture bar).
|
|
*
|
|
* 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
|
|
* the shell takes it.
|
|
*
|
|
* TRACES: UR-066 | DR-112
|
|
*/
|
|
export function shellReservesBottomInset(input: BottomUiVisibilityInput): boolean {
|
|
return !showBottomUi(input);
|
|
}
|