import type { Video, VideoStatus } from "../lib/api"; import { FEED_VIEW_SPEC, type FeedView } from "../lib/feedView"; import VideoCard from "./VideoCard"; import { VideoPinMenu } from "./ComfortPinMenu"; import { useContextMenu } from "./ui/useContextMenu"; import VirtualGrid from "./ui/VirtualGrid"; // Estimated row heights before measurement; real heights are measured per-row so these only affect // the very first paint and the scrollbar's initial guess. Taken from measuring each mode in the // browser rather than guessed — a bad estimate makes the scrollbar jump as you scroll in. // RE-MEASURE these when a mode's layout changes: they have gone stale twice already, most recently // when the bare row became a single line and halved (82 -> 46). The card figures are the roughest // of the set by nature — a card's height follows its column width (measured ~307 at 3 columns, // ~301 at 4, taller still at 2), so no single constant is right for every window. const ROW_EST: Record = { cards: 340, cardsSmall: 257, rows: 115, rowsCompact: 46, tiles: 132, }; export interface VirtualFeedProps { items: Video[]; view: FeedView; onState: (id: string, status: VideoStatus) => void; onToggleSave: (id: string, saved: boolean) => void; onResetState: (id: string) => void; onOpen: (v: Video, startAt?: number | null) => void; hasNextPage: boolean; isFetchingNextPage: boolean; fetchNextPage: () => void; } // The feed's videos on the shared VirtualGrid engine — this wrapper only maps a FeedView to the // grid's layout spec and renders a VideoCard per item. All the virtualization mechanics (measure, // chunk, place, infinite-scroll) live in VirtualGrid. export default function VirtualFeed({ items, view, onState, onToggleSave, onResetState, onOpen, hasNextPage, isFetchingNextPage, fetchNextPage, }: VirtualFeedProps) { const { minCol, colMaxWidth, maxCols, thumb } = FEED_VIEW_SPEC[view]; // The comfort-shelf pin (E24 S3) lives HERE rather than in each of the feed's callers: this is the // list, and `useContextMenu`'s contract is one menu per list. Mounting it at this level is also // what gives the channel page the gesture for free — it renders ``, so it is // the same component all the way down. const pinMenu = useContextMenu