* refactor(virtual): extract scrollMargin measurement into a reusable hook
VirtualCardGrid carried its own useLayoutEffect + ResizeObserver block for
the scroll-margin computation introduced in #764. The same pattern is
needed on every other useVirtualizer site whose wrapper sits below other
content; pull it out as useVirtualizerScrollMargin so each call-site is a
single hook invocation instead of a 20-line duplicate.
No behavior change for VirtualCardGrid — same scroll element, same deps,
same observed nodes.
* fix(virtual): apply scrollMargin to the remaining useVirtualizer call-sites
#764 fixed the symptom for VirtualCardGrid (Albums / Artists grid via the
shared component, Composers grid, "More by …" rail, etc.) but four more
useVirtualizer call-sites have the same shape — wrapper sits below a
sticky page header and TanStack would otherwise place rows starting at
the scroll-element top, unmounting rows that are still in the viewport
and at higher offsets refusing to render at all:
- Artists grid view (Artists.tsx + ArtistsGridView.tsx) — under the
sticky title + filter row + alphabet bar (~150–250 px depending on
alphabet wrap).
- Artists list view (Artists.tsx + ArtistsListView.tsx) — same header
stack; flat letter/row stream means a noticeable jump on long
libraries.
- Composers list view (Composers.tsx) — identical header stack to
Artists.
- VirtualSongList — ~36 px sticky song-list header inside its own
scroll container; the offset was small enough to be absorbed by
overscan, but the pattern was the same so wire it up for consistency.
Each call-site now feeds wrapRef + scroll-element lookup into
useVirtualizerScrollMargin and forwards the value into useVirtualizer +
the translateY of each rendered row.
* docs(changelog): note PR #766 under Fixed in v1.46.0