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
+42
View File
@@ -0,0 +1,42 @@
import { useLayoutEffect, useState, type RefObject } from 'react';
import {
CARD_GRID_MAX_COLS,
type CardGridRowHeightVariant,
computeCardGridColumnCount,
computeCellWidthPx,
estimateRowHeightPx,
} from '../utils/cardGridLayout';
/**
* ResizeObserver-driven column count (max six) and virtual row height estimate
* from the measured cell width.
*/
export function useCardGridMetrics(
measureRef: RefObject<HTMLElement | null>,
observerEnabled: boolean,
variant: CardGridRowHeightVariant,
layoutSignal: number,
): { gridCols: number; rowHeightEst: number } {
const [gridCols, setGridCols] = useState(4);
const [rowHeightEst, setRowHeightEst] = useState(() =>
estimateRowHeightPx(computeCellWidthPx(960, CARD_GRID_MAX_COLS), variant),
);
useLayoutEffect(() => {
if (!observerEnabled) return;
const el = measureRef.current;
if (!el) return;
const onResize = () => {
const w = el.clientWidth;
const cols = computeCardGridColumnCount(w);
setGridCols(cols);
setRowHeightEst(estimateRowHeightPx(computeCellWidthPx(w, cols), variant));
};
onResize();
const ro = new ResizeObserver(onResize);
ro.observe(el);
return () => ro.disconnect();
}, [observerEnabled, variant, layoutSignal]);
return { gridCols, rowHeightEst };
}