{"version":3,"file":"VirtualList.cjs","names":[],"sources":["../../../src/components/VirtualList/VirtualList.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — items, itemHeight, height and overscan are the\n * window maths; renderItem and getKey the row contract; label the accessible name of\n * a scroll region. Nine props is what windowing costs — the alternative is the app\n * doing the maths.\n */\nimport { memo, useEffect, useRef, useState } from \"react\";\nimport type { CSSProperties, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./VirtualList.module.css\";\n\nexport interface VirtualListProps<T> {\n    items: T[];\n    /** Fixed pixel height for each row. */\n    itemHeight: number;\n    /** Render a single row. */\n    renderItem: (item: T, index: number) => ReactNode;\n    /** Container height (px) or any CSS length. */\n    height: number | string;\n    /** Number of items rendered above/below the viewport. Default: 4. */\n    overscan?: number;\n    /** Stable key derivation; defaults to the index. */\n    getKey?: (item: T, index: number) => string | number;\n    className?: string;\n    style?: CSSProperties;\n    /**\n     * Accessible name for the list. Worth setting when the list scrolls: it\n     * then carries a tab stop of its own, and a focus stop that announces\n     * nothing is worse than no focus stop.\n     */\n    label?: string;\n}\n\ninterface VirtualRowProps<T> {\n    item: T;\n    index: number;\n    top: number;\n    height: number;\n    render: (item: T, index: number) => ReactNode;\n}\n\n/**\n * One windowed row, rendered on its own so React can skip it.\n *\n * Without this boundary every scroll event re-rendered the whole visible window\n * rather than the one row that entered it: measured at 126 `renderItem` calls\n * across twenty single-row scroll steps where twenty are needed, and the factor\n * grows with the number of visible rows.\n *\n * `render` is part of the comparison on purpose. Reading it from a ref instead\n * would let a row keep painting output from a stale closure — the row that shows\n * the selected item would never notice the selection changed. Keeping it in the\n * props means an app that wraps `renderItem` in `useCallback` gets the skip, and\n * an app that passes an inline arrow gets exactly today's behaviour.\n *\n * @param props - The row's item, its position and the caller's renderer.\n * @returns The positioned row element.\n */\nfunction VirtualRowImpl<T>({ item, index, top, height, render }: VirtualRowProps<T>) {\n    return (\n        <div role=\"listitem\" className={styles.row} style={{ top, height }}>\n            {render(item, index)}\n        </div>\n    );\n}\n\n/**\n * `memo` erases the generic, so the memoised component is re-typed as the\n * function it wraps. The runtime value is unchanged; only the signature is\n * restored.\n */\nconst VirtualRow = memo(VirtualRowImpl) as typeof VirtualRowImpl;\n\n/**\n * Fixed-height virtual list. Renders only the visible window plus a small\n * overscan buffer. Suitable for lists of thousands of identical rows.\n *\n * For variable heights, use `react-window`/`@tanstack/react-virtual` — those\n * solve a more general problem at the cost of extra setup.\n */\nexport function VirtualList<T>({\n    items,\n    itemHeight,\n    renderItem,\n    height,\n    overscan = 4,\n    getKey,\n    className,\n    style,\n    label,\n}: VirtualListProps<T>) {\n    const containerRef = useRef<HTMLDivElement>(null);\n    const [scrollTop, setScrollTop] = useState<number>(0);\n    const [viewport, setViewport] = useState<number>(0);\n\n    useEffect(() => {\n        const element = containerRef.current;\n        if (!element) return;\n        setViewport(element.clientHeight);\n        if (typeof ResizeObserver === \"undefined\") return;\n        const observer = new ResizeObserver(() => setViewport(element.clientHeight));\n        observer.observe(element);\n        return () => observer.disconnect();\n    }, []);\n\n    const totalHeight = items.length * itemHeight;\n    /*\n     * Rows are absolutely positioned inside the spacer, so nothing in a\n     * virtual list is focusable by default: a keyboard user can see the\n     * scrollbar and has no way to move it. The tab stop only appears while\n     * there is something past the fold — measured from the numbers already at\n     * hand rather than from the DOM.\n     */\n    const scrollable = viewport > 0 && totalHeight - viewport > 1;\n    const start = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);\n    const visibleCount = Math.ceil(viewport / itemHeight) + overscan * 2;\n    const end = Math.min(items.length, start + visibleCount);\n\n    return (\n        <div\n            ref={containerRef}\n            className={cn(styles.scroll, className)}\n            style={{ height, ...style }}\n            onScroll={(event) => setScrollTop((event.target as HTMLDivElement).scrollTop)}\n            role=\"list\"\n            aria-label={label}\n            tabIndex={scrollable ? 0 : undefined}\n        >\n            <div className={styles.spacer} style={{ height: totalHeight }}>\n                {items.slice(start, end).map((item, offset) => {\n                    const index = start + offset;\n                    return (\n                        <VirtualRow\n                            key={getKey ? getKey(item, index) : index}\n                            item={item}\n                            index={index}\n                            top={index * itemHeight}\n                            height={itemHeight}\n                            render={renderItem}\n                        />\n                    );\n                })}\n            </div>\n        </div>\n    );\n}\n"],"mappings":"kIA0DA,SAAS,EAAkB,CAAE,OAAM,QAAO,MAAK,SAAQ,UAA8B,CACjF,OACI,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,KAAK,WAAW,UAAW,EAAA,QAAO,IAAK,MAAO,CAAE,MAAK,QAAO,EAC5D,SAAA,EAAO,EAAM,CAAK,CAClB,CAAA,CAEb,CAOA,IAAM,GAAA,EAAa,EAAA,KAAA,CAAK,CAAc,EAStC,SAAgB,EAAe,CAC3B,QACA,aACA,aACA,SACA,WAAW,EACX,SACA,YACA,QACA,SACoB,CACpB,IAAM,GAAA,EAAe,EAAA,OAAA,CAAuB,IAAI,EAC1C,CAAC,EAAW,IAAA,EAAgB,EAAA,SAAA,CAAiB,CAAC,EAC9C,CAAC,EAAU,IAAA,EAAe,EAAA,SAAA,CAAiB,CAAC,GAElD,EAAA,EAAA,UAAA,KAAgB,CACZ,IAAM,EAAU,EAAa,QAG7B,GAFI,CAAC,IACL,EAAY,EAAQ,YAAY,EAC5B,OAAO,eAAmB,KAAa,OAC3C,IAAM,EAAW,IAAI,mBAAqB,EAAY,EAAQ,YAAY,CAAC,EAE3E,OADA,EAAS,QAAQ,CAAO,MACX,EAAS,WAAW,CACrC,EAAG,CAAC,CAAC,EAEL,IAAM,EAAc,EAAM,OAAS,EAQ7B,EAAa,EAAW,GAAK,EAAc,EAAW,EACtD,EAAQ,KAAK,IAAI,EAAG,KAAK,MAAM,EAAY,CAAU,EAAI,CAAQ,EACjE,EAAe,KAAK,KAAK,EAAW,CAAU,EAAI,EAAW,EAC7D,EAAM,KAAK,IAAI,EAAM,OAAQ,EAAQ,CAAY,EAEvD,OACI,EAAA,EAAA,IAAA,CAAC,MAAD,CACI,IAAK,EACL,UAAW,EAAA,GAAG,EAAA,QAAO,OAAQ,CAAS,EACtC,MAAO,CAAE,SAAQ,GAAG,CAAM,EAC1B,SAAW,GAAU,EAAc,EAAM,OAA0B,SAAS,EAC5E,KAAK,OACL,aAAY,EACZ,SAAU,EAAa,EAAI,IAAA,GAE3B,UAAA,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,OAAQ,MAAO,CAAE,OAAQ,CAAY,EACvD,SAAA,EAAM,MAAM,EAAO,CAAG,CAAC,CAAC,KAAK,EAAM,IAAW,CAC3C,IAAM,EAAQ,EAAQ,EACtB,OACI,EAAA,EAAA,IAAA,CAAC,EAAD,CAEU,OACC,QACP,IAAK,EAAQ,EACb,OAAQ,EACR,OAAQ,CACX,EANQ,EAAS,EAAO,EAAM,CAAK,EAAI,CAMvC,CAET,CAAC,CACA,CAAA,CACJ,CAAA,CAEb"}