@@ -41,8 +41,12 @@ export function ActiveSectionProvider({ children }: { children: ReactNode }) {
4141
4242 if ( pairs . length === 0 ) return
4343
44- // Match the observer band: a heading becomes active in the top 15% of the viewport.
45- const bandThreshold = ( ) => window . innerHeight * 0.15
44+ // Floor the 15% band 1px past the :target scroll margin so short viewports activate a jumped-to heading.
45+ const bandThreshold = ( ) => {
46+ const target = document . querySelector ( ':target' )
47+ const margin = target ? parseFloat ( getComputedStyle ( target ) . scrollMarginTop ) || 0 : 0
48+ return Math . max ( window . innerHeight * 0.15 , margin + 1 )
49+ }
4650
4751 // Actual scroll positions keep upward scrolling tied to the previous heading or Overview.
4852 const computeActive = ( ) => {
@@ -80,13 +84,16 @@ export function ActiveSectionProvider({ children }: { children: ReactNode }) {
8084 for ( const { el } of pairs ) observer . observe ( el )
8185 window . addEventListener ( 'scroll' , scheduleUpdate , { passive : true } )
8286 window . addEventListener ( 'resize' , scheduleUpdate )
87+ // A hash change can move :target and its threshold without any scroll or intersection change.
88+ window . addEventListener ( 'hashchange' , scheduleUpdate )
8389 computeActive ( )
8490
8591 return ( ) => {
8692 if ( frame ) cancelAnimationFrame ( frame )
8793 observer . disconnect ( )
8894 window . removeEventListener ( 'scroll' , scheduleUpdate )
8995 window . removeEventListener ( 'resize' , scheduleUpdate )
96+ window . removeEventListener ( 'hashchange' , scheduleUpdate )
9097 }
9198 // Re-run when platform or tool selection changes which headings are visible.
9299 } , [ miniTocItems , selectedPlatform , tool ] )
0 commit comments