The last few letters of the A-Z index sat behind the mini player and bottom
nav, where they could not be tapped — most visible on an album's track list or
the music library, since that is where audio is usually already playing.
AlphabetScrollBar sized itself as window.innerHeight minus a hardcoded
bottomGap: 5rem, 7rem or 11rem, chosen by platform and by whether the mini
player was showing. That arithmetic dates from when the mini player and bottom
nav were position: fixed overlays. They have been in-flow flex siblings below
the scroller since BottomUi (DR-009), so the scroller's own bottom edge *is*
the top of the mini player and can simply be measured.
The guess was also short on every device with a navigation or gesture bar,
because --safe-bottom is padded inside BottomUi (DR-112) and no guess knew
about it. Measured against a 800px viewport: the strip overran the scrollport
by ~45px with the nav alone, ~18px with the mini player and ~50px in remote
mode, burying one to three letters.
The floor is now the nearest scrollable ancestor's bottom edge. That ancestor
is resolved by computed overflow-y rather than closest("main"): the root shell
scrolls in a plain div, and a miss fell back to the viewport silently, which
would reinstate the bug on any route outside /library. Observing that scroller
for resize is also what re-measures when the mini player appears, so the
component no longer subscribes to player or platform stores at all.
The floor rule is extracted to alphabetStrip.ts so it can be tested; the three
overlap cases above fail against the old arithmetic and pass against the new.
TRACES: UR-007 | DR-262 | UT-235, UT-236, UT-237
55 lines
2.1 KiB
TypeScript
55 lines
2.1 KiB
TypeScript
/**
|
|
* Pure geometry for the A-Z jump strip (see AlphabetScrollBar.svelte).
|
|
*
|
|
* The strip stretches from wherever it sits down to a floor, and the floor is
|
|
* the whole question: get it wrong and the tail of the alphabet renders past
|
|
* the bottom of the scroller, under the mini player / bottom nav, where it
|
|
* cannot be tapped.
|
|
*
|
|
* The floor is the scroller's own bottom edge — never the viewport's. The strip
|
|
* used to size itself as `window.innerHeight` minus a hardcoded guess at the
|
|
* bars' height (5rem/7rem/11rem by platform and mini-player visibility), which
|
|
* dates from when those bars were `position: fixed` overlays. They are in-flow
|
|
* flex siblings below the scroller now (see BottomUi.svelte), so the scroller's
|
|
* bottom edge *is* the top of the mini player, measured exactly, every frame —
|
|
* and the guess was short on every device with a navigation/gesture bar,
|
|
* because `--safe-bottom` is padded inside BottomUi and the guess never knew
|
|
* about it.
|
|
*
|
|
* Extracted from the component so the floor rule is unit-testable — the
|
|
* component only supplies measurements.
|
|
*
|
|
* TRACES: UR-007 | DR-007, DR-262
|
|
*/
|
|
|
|
/**
|
|
* Breathing room left under the last letter, in px. Mirrors the `top-2` sticky
|
|
* offset at the other end so the strip sits symmetrically in the scrollport.
|
|
*/
|
|
export const STRIP_BOTTOM_GAP = 8;
|
|
|
|
export interface StripHeightInput {
|
|
/** Viewport-relative top of the strip container (`getBoundingClientRect().top`). */
|
|
stripTop: number;
|
|
/**
|
|
* Viewport-relative bottom edge of the scroll container the strip lives in,
|
|
* or `null` when the strip has no scrollable ancestor to measure.
|
|
*/
|
|
scrollerBottom: number | null;
|
|
/** Viewport height — the fallback floor when there is no scroll container. */
|
|
viewportHeight: number;
|
|
/** Override for {@link STRIP_BOTTOM_GAP}, in px. */
|
|
gap?: number;
|
|
}
|
|
|
|
/** How tall the A-Z strip may be without running under the bottom bars. */
|
|
export function stripHeightFor({
|
|
stripTop,
|
|
scrollerBottom,
|
|
viewportHeight,
|
|
gap = STRIP_BOTTOM_GAP,
|
|
}: StripHeightInput): number {
|
|
const floor = scrollerBottom ?? viewportHeight;
|
|
return Math.max(0, floor - gap - stripTop);
|
|
}
|