@@ -16,8 +16,6 @@ interface HeadingNode {
1616}
1717
1818interface DocumentOutlineProps {
19- /** Ref to the container element that holds the rendered markdown (.ghmd) */
20- contentRef : React . RefObject < HTMLDivElement | null > ;
2119 /** Whether the outline should be visible (only in preview mode) */
2220 visible : boolean ;
2321}
@@ -168,7 +166,7 @@ function FlatOutlineItem({
168166// Main component
169167// ---------------------------------------------------------------------------
170168
171- export function DocumentOutline ( { contentRef , visible } : DocumentOutlineProps ) {
169+ export function DocumentOutline ( { visible } : DocumentOutlineProps ) {
172170 const [ headings , setHeadings ] = useState < HeadingNode [ ] > ( [ ] ) ;
173171 const [ activeId , setActiveId ] = useState < string | null > ( null ) ;
174172 const [ isExpanded , setIsExpanded ] = useState ( ( ) => {
@@ -180,6 +178,12 @@ export function DocumentOutline({ contentRef, visible }: DocumentOutlineProps) {
180178 const [ focusedIndex , setFocusedIndex ] = useState ( - 1 ) ;
181179 const [ mobileOpen , setMobileOpen ] = useState ( false ) ;
182180
181+ const scrollContainerRef = useRef < HTMLElement | null > ( null ) ;
182+ useEffect ( ( ) => {
183+ scrollContainerRef . current =
184+ document . querySelector < HTMLElement > ( "[data-scroll-container]" ) ?? null ;
185+ } , [ ] ) ;
186+
183187 const outlineRef = useRef < HTMLDivElement > ( null ) ;
184188 const listRef = useRef < HTMLDivElement > ( null ) ;
185189 const searchInputRef = useRef < HTMLInputElement > ( null ) ;
@@ -195,7 +199,7 @@ export function DocumentOutline({ contentRef, visible }: DocumentOutlineProps) {
195199 // Extract headings from rendered content
196200 useEffect ( ( ) => {
197201 if ( ! visible ) return ;
198- const container = contentRef . current ;
202+ const container = scrollContainerRef . current ;
199203 if ( ! container ) return ;
200204
201205 // Wait for next frame to ensure markdown is rendered
@@ -207,27 +211,46 @@ export function DocumentOutline({ contentRef, visible }: DocumentOutlineProps) {
207211 } ) ;
208212
209213 return ( ) => cancelAnimationFrame ( raf ) ;
210- } , [ contentRef , visible ] ) ;
214+ } , [ visible ] ) ;
211215
212216 // Deep-link: scroll to hash on mount
213217 useEffect ( ( ) => {
214- if ( ! visible || headings . length === 0 ) return ;
218+ if ( ! visible ) return ;
215219 const hash = window . location . hash . slice ( 1 ) ;
216- if ( hash ) {
220+ if ( ! hash ) return ;
221+
222+ const container = scrollContainerRef . current ;
223+ if ( ! container ) return ;
224+
225+ const scrollParent =
226+ container . closest < HTMLElement > ( "[data-scroll-container]" ) ?? null ;
227+
228+ const raf = requestAnimationFrame ( ( ) => {
217229 const el = document . getElementById ( hash ) ;
218- if ( el ) {
219- setTimeout ( ( ) => {
220- el . scrollIntoView ( { behavior : "smooth" , block : "start" } ) ;
221- } , 100 ) ;
222- setActiveId ( hash ) ;
230+ if ( ! el ) return ;
231+
232+ if ( scrollParent ) {
233+ const containerRect = scrollParent . getBoundingClientRect ( ) ;
234+ const elRect = el . getBoundingClientRect ( ) ;
235+ scrollParent . scrollTop =
236+ scrollParent . scrollTop +
237+ elRect . top -
238+ containerRect . top -
239+ SCROLL_OFFSET ;
240+ } else {
241+ el . scrollIntoView ( { block : "start" } ) ;
223242 }
224- }
225- } , [ visible , headings ] ) ;
243+
244+ setActiveId ( hash ) ;
245+ } ) ;
246+
247+ return ( ) => cancelAnimationFrame ( raf ) ;
248+ } , [ visible ] ) ;
226249
227250 // Scroll tracking with IntersectionObserver
228251 useEffect ( ( ) => {
229252 if ( ! visible || headings . length === 0 ) return ;
230- const container = contentRef . current ;
253+ const container = scrollContainerRef . current ;
231254 if ( ! container ) return ;
232255
233256 const flat = flattenTree ( headings ) ;
@@ -240,7 +263,7 @@ export function DocumentOutline({ contentRef, visible }: DocumentOutlineProps) {
240263
241264 // Find the scroll parent
242265 const scrollParent =
243- container . closest < HTMLElement > ( "[class*='overflow-y-auto' ]" ) ?? window ;
266+ container . closest < HTMLElement > ( "[data-scroll-container ]" ) ?? window ;
244267
245268 if ( "IntersectionObserver" in window ) {
246269 const rootEl = scrollParent instanceof HTMLElement ? scrollParent : null ;
@@ -366,7 +389,7 @@ export function DocumentOutline({ contentRef, visible }: DocumentOutlineProps) {
366389 clearTimeout ( timer ) ;
367390 target . removeEventListener ( "scroll" , handleScroll ) ;
368391 } ;
369- } , [ visible , headings , contentRef ] ) ;
392+ } , [ visible , headings ] ) ;
370393
371394 // Auto-scroll outline to keep active item visible
372395 useEffect ( ( ) => {
@@ -391,14 +414,30 @@ export function DocumentOutline({ contentRef, visible }: DocumentOutlineProps) {
391414 const el = document . getElementById ( id ) ;
392415 if ( ! el ) return ;
393416
417+ const container = scrollContainerRef . current ;
418+ if ( ! container ) return ;
419+
420+ const scrollParent =
421+ container . closest < HTMLElement > ( "[data-scroll-container]" ) ?? null ;
422+
423+ if ( scrollParent ) {
424+ const containerRect = scrollParent . getBoundingClientRect ( ) ;
425+ const elRect = el . getBoundingClientRect ( ) ;
426+ const targetScrollTop =
427+ scrollParent . scrollTop +
428+ elRect . top -
429+ containerRect . top -
430+ SCROLL_OFFSET ;
431+ scrollParent . scrollTop = targetScrollTop ;
432+ } else {
433+ el . scrollIntoView ( { behavior : "smooth" , block : "start" } ) ;
434+ }
435+
394436 setActiveId ( id ) ;
395437 setMobileOpen ( false ) ;
396438
397439 // Update hash without full reload
398440 history . replaceState ( null , "" , `#${ id } ` ) ;
399-
400- // Smooth scroll
401- el . scrollIntoView ( { behavior : "smooth" , block : "start" } ) ;
402441 } , [ ] ) ;
403442
404443 // Keyboard navigation
0 commit comments