Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit 3425e755790b87640a5e7091dbf09a686957e967
parent 284472d2feea6d80ca1885c693ca3cadb6c61250
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Fri, 22 May 2026 13:08:16 -0400

fix virtual list gaps

Diffstat:
Mcommon/components/TranscriptSearch.tsx | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
1 file changed, 64 insertions(+), 31 deletions(-)

diff --git a/common/components/TranscriptSearch.tsx b/common/components/TranscriptSearch.tsx @@ -1508,7 +1508,16 @@ type ResultRow = hitIndex: number; isFirstInCard: false; isLastInCard: boolean; - }; + } + // A dedicated 12px transparent row that sits between cards. Doing the + // gap as its own virtualized row (rather than padding-bottom on the + // previous card's last row) means the row that "owns" the gap has its + // height pinned to a constant — the virtualizer never has to discover + // it through ResizeObserver, so the gap stays put even before the + // dynamic-measurement pass completes. + | { kind: "gap"; cardIndex: number }; + +const GAP_ROW_PX = 12; function buildResultRows( resultGroups: ReadonlyArray<ResultGroup>, @@ -1554,15 +1563,19 @@ function buildResultRows( }); }); } - // Flag the last row of this card so the renderer can close its visual - // grouping (rounded-b, border-b, margin to the next card). + // Mark the last content row of this card so the renderer can apply + // the rounded-b / border-b. Tombstone cards (header only) get the + // header marked. const last = out[out.length - 1]; - if (last && last.cardIndex === cardIndex && out.length - 1 > cardStart) { - (last as { isLastInCard: boolean }).isLastInCard = true; - } else if (last && out.length - 1 === cardStart) { - // Card with no contributing hits — header is also the last row. + if (last && last.kind !== "gap" && last.cardIndex === cardIndex) { (last as { isLastInCard: boolean }).isLastInCard = true; } + // Inter-card gap, skipped after the final card so there's no + // trailing empty space below the result list. + if (cardIndex < resultGroups.length - 1) { + out.push({ kind: "gap", cardIndex }); + } + void cardStart; }); return out; } @@ -1575,6 +1588,8 @@ function rowKey(row: ResultRow): string { return `c${row.cardIndex}:l:${row.leafId}`; case "hit": return `c${row.cardIndex}:h:${row.leafId}:${row.hitIndex}`; + case "gap": + return `c${row.cardIndex}:gap`; } } @@ -1621,7 +1636,11 @@ function VirtualResultList({ const rowVirtualizer = useWindowVirtualizer({ count: flatRows.length, - estimateSize: () => RESULT_ROW_ESTIMATE_PX, + // The estimate only matters until ResizeObserver measures each row. + // Picking a value close to the most common row height (transcript + // hits + leaf-headers) keeps any pre-measurement layout shift small. + estimateSize: (index) => + flatRows[index]?.kind === "gap" ? GAP_ROW_PX : RESULT_ROW_ESTIMATE_PX, overscan: 12, scrollMargin, }); @@ -1637,30 +1656,39 @@ function VirtualResultList({ > {virtualItems.map((vi) => { const row = flatRows[vi.index]; - // The outer (positioning) wrapper carries `pb-3` on last-in-card - // rows so the next card starts 12px lower. Margins are no good - // here — sibling absolutely-positioned rows ignore them, but the - // virtualizer's ResizeObserver picks up padding as part of the - // measured row height, which pushes subsequent translateY values. - // The inner div holds the actual visual frame (border + bg + - // rounded corners) so the gap appears OUTSIDE the card. + if (row.kind === "gap") { + // Fixed-height transparent row. Putting the frame classes on + // the same div the virtualizer measures (rather than wrapping + // an inner frame div) avoids a stale-measurement class of + // bugs where the gap "appeared" between rows until a scroll + // forced re-measurement. + return ( + <VirtualRow + key={rowKey(row)} + measureRef={rowVirtualizer.measureElement} + index={vi.index} + offset={vi.start - scrollMargin} + className="" + > + <div style={{ height: GAP_ROW_PX }} aria-hidden="true" /> + </VirtualRow> + ); + } return ( <VirtualRow key={rowKey(row)} measureRef={rowVirtualizer.measureElement} index={vi.index} offset={vi.start - scrollMargin} - className={row.isLastInCard ? "pb-3" : ""} + className={rowFrameClass(row)} > - <div className={rowFrameClass(row)}> - <ResultRowView - row={row} - leavesById={leavesById} - activeVideo={activeVideo} - activeTime={activeTime} - openWithMode={openWithMode} - /> - </div> + <ResultRowView + row={row} + leavesById={leavesById} + activeVideo={activeVideo} + activeTime={activeTime} + openWithMode={openWithMode} + /> </VirtualRow> ); })} @@ -1668,12 +1696,17 @@ function VirtualResultList({ ); } -// Tailwind classes for the inner card-frame div. Side borders on every row, -// plus a top border on every non-first row, give the divide-y effect; the -// first and last rows of a card carry the rounded corners. -function rowFrameClass(row: ResultRow): string { +// Tailwind classes for the row's outer (virtualized + measured) div. Side +// borders on every row plus a top border on every non-first row of a card +// give the divide-y effect; the first and last rows of a card carry the +// rounded corners. The frame lives on the measured div directly so the +// virtualizer's ResizeObserver sees the row's true height without an +// intermediate wrapper. +function rowFrameClass( + row: Exclude<ResultRow, { kind: "gap" }>, +): string { const base = - "border-l border-r border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900"; + "border-l border-r border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 overflow-hidden"; const top = row.isFirstInCard ? "rounded-t-lg border-t" : "border-t border-zinc-200/60 dark:border-zinc-800/60"; @@ -1688,7 +1721,7 @@ function ResultRowView({ activeTime, openWithMode, }: { - row: ResultRow; + row: Exclude<ResultRow, { kind: "gap" }>; leavesById: ReadonlyMap<string, LeafInfo>; activeVideo: string | null; activeTime: number | null;