diff --git a/frontend/src/components/timeline/Timeline.tsx b/frontend/src/components/timeline/Timeline.tsx index 8d8b34b..c9dbc01 100644 --- a/frontend/src/components/timeline/Timeline.tsx +++ b/frontend/src/components/timeline/Timeline.tsx @@ -145,6 +145,20 @@ export function Timeline() { [photos, columns, sortBy] ) + // Pre-computed offset of every header in the virtualizer's coordinate + // space, used to drive the sticky-header overlay below. + const headerOffsets = useMemo(() => { + const result: { offset: number; label: string }[] = [] + let cumulative = 0 + for (const item of items) { + if (item.type === 'header') { + result.push({ offset: cumulative, label: item.label }) + } + cumulative += item.height + } + return result + }, [items]) + // Range-selection helper. Operates on the global photos array, not on // virtualizer items. const selectRange = (endIndex: number) => { @@ -171,6 +185,32 @@ export function Timeline() { virtualizer.measure() }, [items, virtualizer]) + // Track scroll position so we can show the current group label as a + // pinned overlay at the top of the scroll container. The virtualizer's + // items use transform translateY (so CSS position: sticky doesn't work + // on the inline headers); the overlay sidesteps that by living outside + // the virtualizer's positioned children. + const [scrollTop, setScrollTop] = useState(0) + useEffect(() => { + const el = parentRef.current + if (!el) return + const onScroll = () => setScrollTop(el.scrollTop) + el.addEventListener('scroll', onScroll, { passive: true }) + return () => el.removeEventListener('scroll', onScroll) + }, []) + + // Find the latest header whose start <= scrollTop. That's the label of + // the group containing whatever is currently at the top of the viewport. + const stickyLabel = useMemo(() => { + if (headerOffsets.length === 0) return null + let current: string | null = null + for (const h of headerOffsets) { + if (h.offset <= scrollTop) current = h.label + else break + } + return current + }, [headerOffsets, scrollTop]) + // Measure container width on mount and resize. useEffect(() => { const measureWidth = () => { @@ -267,11 +307,23 @@ export function Timeline() { } return ( -