-
+
+ {heavy ? (
+
+ ) : (
+
+ )}
)
}
-function ReviewNodeList({
- animate,
- depth,
- motion: useMotion,
- nodes
-}: {
- animate: boolean
- depth: number
- motion: boolean
- nodes: ReviewTreeNode[]
-}) {
- // Heavy lists: plain rows + content-visibility, no motion.
- if (!useMotion) {
- return (
- <>
- {nodes.map(node => (
-
- {node.isDir ? (
-
- ) : (
-
- )}
-
- ))}
- >
- )
- }
-
+function ReviewNodeList({ animate, depth, nodes }: { animate: boolean; depth: number; nodes: ReviewTreeNode[] }) {
return (
{nodes.map(node => (
@@ -170,7 +190,7 @@ function ReviewNodeList({
transition={animate ? ROW_TRANSITION : ROW_INSTANT}
>
{node.isDir ? (
-
+
) : (
)}
@@ -180,6 +200,66 @@ function ReviewNodeList({
)
}
+// Virtualized heavy list: the scroller mounts only the rows intersecting the
+// viewport (plus overscan), so a folder with tens of thousands of changed
+// files never materializes every row in the DOM. Rows are absolutely
+// positioned inside a spacer sized to the full list, which keeps the
+// scrollbar honest. Folders render as `leaf` rows — their children come from
+// the flattened row list, not inline — so expanding one just grows the list.
+function VirtualizedReviewList({
+ rows,
+ scrollRef
+}: {
+ rows: ReviewFlatRow[]
+ scrollRef: RefObject
+}) {
+ const virtualizer = useVirtualizer({
+ count: rows.length,
+ estimateSize: () => ROW_HEIGHT,
+ getItemKey: index => rows[index]?.node.id ?? index,
+ getScrollElement: () => scrollRef.current,
+ // jsdom-friendly default; the real rect takes over on first observe.
+ initialRect: { height: 600, width: 240 },
+ overscan: OVERSCAN_ROWS
+ })
+
+ const virtualItems = virtualizer.getVirtualItems()
+ const totalSize = virtualizer.getTotalSize()
+
+ return (
+
+ {virtualItems.map(virtualItem => {
+ const row = rows[virtualItem.index]
+
+ if (!row) {
+ return null
+ }
+
+ return (
+
+ {row.node.isDir ? (
+
+ ) : (
+
+ )}
+
+ )
+ })}
+
+ )
+}
+
// Depth-0 rows align their icon to the panel header's dither glyph: the tree
// body has px-1 (4px) and the header glyph sits at px-2.5 (10px) + the label's
// pl-2 (8px) = 18px, so the base inset is 18 − 4 = 14px.
@@ -191,19 +271,22 @@ function rowStyle(depth: number): CSSProperties {
function ReviewDirRow({
animate,
+ defaultOpen = true,
depth,
- motion: useMotion,
+ leaf = false,
node
}: {
animate: boolean
+ defaultOpen?: boolean
depth: number
- motion: boolean
+ /** Virtualized rows render their children from the flattened row list, not inline. */
+ leaf?: boolean
node: ReviewTreeNode
}) {
const nodeOpen = useStore($sidebarWorkspaceNodeOpen)
const id = `review:${node.id}`
- const open = nodeOpen[id] ?? true
- const toggle = () => toggleWorkspaceNodeCollapsed(id)
+ const open = nodeOpen[id] ?? defaultOpen
+ const toggle = () => toggleWorkspaceNodeCollapsed(id, defaultOpen)
return (
<>
@@ -222,8 +305,8 @@ function ReviewDirRow({
{!open && }
- {open && node.children && (
-