feat(ui): shared card grid with max six columns and virtualization

Add cardGridLayout helpers, useCardGridMetrics, useRemeasureGridVirtualizer, and VirtualCardGrid (TanStack row virtualization, always remeasure on layout changes).

Apply to Artists (grid + list unchanged policy), Albums, Composers grid, playlists, radio stations, offline library, and album-heavy browse/detail pages. Respect disableMainstageVirtualLists for a non-virtual grid with the same column rules.

Includes vitest coverage for column cap.
This commit is contained in:
Maxim Isaev
2026-05-15 02:05:34 +03:00
parent 778c9e00fd
commit 7c6d3694d4
20 changed files with 601 additions and 202 deletions
+12 -5
View File
@@ -11,6 +11,7 @@ import { useVirtualizer } from '@tanstack/react-virtual';
import { APP_MAIN_SCROLL_VIEWPORT_ID } from '../constants/appScroll';
import { useElementClientHeightById } from '../hooks/useResizeClientHeight';
import { usePerfProbeFlags } from '../utils/perf/perfFlags';
import { VirtualCardGrid } from '../components/VirtualCardGrid';
const ALL_SENTINEL = 'ALL';
const ALPHABET = [ALL_SENTINEL, '#', ...'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('')];
@@ -276,10 +277,16 @@ export default function Composers() {
{loading && <div style={{ display: 'flex', justifyContent: 'center', padding: '3rem' }}><div className="spinner" /></div>}
{!loading && viewMode === 'grid' && (
<div className="composer-grid-wrap">
{visible.map(artist => (
<VirtualCardGrid
items={visible}
itemKey={(a, _i) => a.id}
rowVariant="composer"
disableVirtualization={perfFlags.disableMainstageVirtualLists}
layoutSignal={visible.length}
wrapClassName="composer-grid-wrap"
gridGap="var(--space-2)"
renderItem={artist => (
<div
key={artist.id}
className="composer-card"
onClick={() => navigate(`/composer/${artist.id}`)}
onContextMenu={(e) => {
@@ -294,8 +301,8 @@ export default function Composers() {
</div>
)}
</div>
))}
</div>
)}
/>
)}
{!loading && viewMode === 'list' && (