diff --git a/app/globals.css b/app/globals.css index de5e0ecd..80de871f 100644 --- a/app/globals.css +++ b/app/globals.css @@ -620,6 +620,9 @@ pre.shiki { .footnote-side-item:hover, .footnote-side-item:focus-within, .footnote-side-item.footnote-side-focused { - max-height: 40em; + max-height: none; + overflow: visible; opacity: var(--bg-page-alpha); + z-index: 1; + background: rgba(var(--bg-page), 1); } diff --git a/components/Footnotes/FootnoteSideColumnLayout.tsx b/components/Footnotes/FootnoteSideColumnLayout.tsx index ae9007ba..71411589 100644 --- a/components/Footnotes/FootnoteSideColumnLayout.tsx +++ b/components/Footnotes/FootnoteSideColumnLayout.tsx @@ -20,7 +20,6 @@ export function FootnoteSideColumnLayout(props: { let containerRef = useRef(null); let innerRef = useRef(null); let [positions, setPositions] = useState<(T & { top: number })[]>([]); - let [scrollOffset, setScrollOffset] = useState(0); let calculatePositions = useCallback(() => { let container = containerRef.current; @@ -36,7 +35,9 @@ export function FootnoteSideColumnLayout(props: { let scrollTop = scrollWrapper.scrollTop; let scrollWrapperRect = scrollWrapper.getBoundingClientRect(); - setScrollOffset(scrollTop); + + // Sync scroll transform directly on the DOM (no React re-render) + inner.style.transform = `translateY(-${scrollTop}px)`; let measurements: (T & { anchorTop: number; height: number })[] = []; @@ -79,24 +80,60 @@ export function FootnoteSideColumnLayout(props: { ?.querySelector(".pageScrollWrapper") as HTMLElement | null; if (!scrollWrapper) return; + // On scroll, update the transform directly without React re-render let onScroll = () => { - setScrollOffset(scrollWrapper!.scrollTop); + let inner = innerRef.current; + if (inner) { + inner.style.transform = `translateY(-${scrollWrapper!.scrollTop}px)`; + } }; - scrollWrapper.addEventListener("scroll", onScroll); + scrollWrapper.addEventListener("scroll", onScroll, { passive: true }); + + // Forward wheel events from the side column to the scroll wrapper + let container = containerRef.current!; + let onWheel = (e: WheelEvent) => { + scrollWrapper!.scrollTop += e.deltaY; + }; + container.addEventListener("wheel", onWheel, { passive: true }); let resizeObserver = new ResizeObserver(calculatePositions); resizeObserver.observe(scrollWrapper); - let mutationObserver = new MutationObserver(calculatePositions); + // Observe all side items so positions recalculate when their heights change + let observeSideItems = () => { + let inner = innerRef.current; + if (!inner) return; + for (let el of inner.querySelectorAll("[data-footnote-side-id]")) { + resizeObserver.observe(el); + } + }; + observeSideItems(); + + let mutationObserver = new MutationObserver(() => { + calculatePositions(); + // Re-observe in case new items were added + observeSideItems(); + }); mutationObserver.observe(scrollWrapper, { childList: true, subtree: true, characterData: true, }); + // Also observe the inner container so we recalculate when side items + // are added/removed (they're siblings of scrollWrapper, not children) + let innerEl = innerRef.current; + if (innerEl) { + mutationObserver.observe(innerEl, { + childList: true, + subtree: true, + }); + } + return () => { scrollWrapper!.removeEventListener("scroll", onScroll); + container.removeEventListener("wheel", onWheel); resizeObserver.disconnect(); mutationObserver.disconnect(); }; @@ -107,7 +144,7 @@ export function FootnoteSideColumnLayout(props: { return (