import type { Video, VideoStatus } from "../lib/api"; import { FEED_VIEW_SPEC, type FeedView } from "../lib/feedView"; import type { NEXT_PAGE } from "../lib/nextPage"; import VideoCard from "./VideoCard"; import { VideoMenu } from "./VideoMenu"; 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 E28 C1 action row moved onto the card's picture (cards 340 -> 262, small 257 -> 226). // The card figures are the roughest of the set by nature — a card's height follows its column width // (262 measured at 4 columns in a 1268px window), so no single constant is right for every window. const ROW_EST: Record = { cards: 262, cardsSmall: 226, rows: 117, rowsCompact: 46, tiles: 138, }; 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: (options: typeof NEXT_PAGE) => unknown; } // 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 card menu (the video's whole action list, E28 C1) 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 menu for free — it renders // ``, so it is the same component all the way down. const menu = useContextMenu