33 <BNavbar v-b-color-mode =" 'dark'" variant =" primary" sticky =" top" toggleable =" lg" :container =" true" >
44 <BToastOrchestrator />
55 <BModalOrchestrator />
6+ <BPopoverOrchestrator />
67 <div class =" d-flex gap-2 align-items-center" >
78 <BNavbarToggle v-b-toggle.sidebar-menu />
89 <BNavbarBrand :to =" withBase('/')" class =" p-0 me-0 me-lg-2" >
137138 header-class =" pb-0 d-flex offcanvas-hidden-width"
138139 body-class =" py-2"
139140 >
140- <PageContents />
141+ <PageContents :contents = " contents " :active-id = " activeId " />
141142 </BOffcanvas >
142143 </ClientOnly >
143144 </aside >
@@ -163,6 +164,7 @@ import {
163164 BNavItem ,
164165 BNavItemDropdown ,
165166 BOffcanvas ,
167+ BPopoverOrchestrator ,
166168 BRow ,
167169 BToastOrchestrator ,
168170 useColorMode ,
@@ -191,6 +193,7 @@ import {VPNavBarSearch} from 'vitepress/theme'
191193import {appInfoKey } from ' ./keys'
192194import {useMediaQuery } from ' @vueuse/core'
193195import PageContents from ' ../../src/components/PageContents.vue'
196+ import {type ContentsItem , type HeaderItem } from ' ../../src/types'
194197
195198// https://vitepress.dev/reference/runtime-api#usedata
196199const {page} = useData ()
@@ -199,9 +202,11 @@ const route = useRoute()
199202const content = useTemplateRef <ComponentPublicInstance <HTMLElement >>(' _content' )
200203const target = useTemplateRef <ComponentPublicInstance <HTMLElement >>(' _target' )
201204
202- useScrollspy (content , target , {
203- contentQuery: ' :scope > div > [id], #component-reference' ,
205+ const { current : activeId, list : items} = useScrollspy (content , target , {
206+ contentQuery: ' :scope > div > [id], #component-reference, .component-reference h3 ' ,
204207 targetQuery: ' :scope [href]' ,
208+ rootMargin: ' 0px 0px -25%' ,
209+ manual: true ,
205210})
206211
207212const globalData = inject (appInfoKey , {
@@ -293,6 +298,49 @@ const set = (newValue: keyof typeof map) => {
293298 colorMode .value = newValue
294299}
295300
301+ const headers = computed (() =>
302+ items .value .map ((item ) => {
303+ const rawTag = item .el ?.tagName ?.toUpperCase () ?? ' '
304+ const isHeading = / ^ H[1-6 ] $ / .test (rawTag )
305+ const tag = isHeading ? rawTag : ' DIV'
306+ const level = tag .startsWith (' H' ) ? parseInt (tag .replace (' H' , ' ' )) : 3
307+ return {
308+ ... item ,
309+ tag ,
310+ level ,
311+ } as HeaderItem
312+ })
313+ )
314+
315+ const contents = computed (() => {
316+ const root: ContentsItem [] = []
317+ const stack: ContentsItem [] = []
318+
319+ headers .value .forEach ((header ) => {
320+ const item = {... header , children: [] as ContentsItem []} as ContentsItem
321+
322+ while (stack .length && stack [stack .length - 1 ].level >= item .level ) {
323+ stack .pop ()
324+ }
325+
326+ if (stack .length === 0 ) {
327+ root .push (item )
328+ } else {
329+ stack [stack .length - 1 ].children .push (item )
330+ }
331+
332+ stack .push (item )
333+ })
334+
335+ if (root .length !== 1 ) {
336+ // Something isn't right if we have no root items or more than one root item
337+ // eslint-disable-next-line no-console
338+ console .warn (' Unexpected header structure:' , headers , ' Root items:' , root )
339+ }
340+
341+ return root .length > 0 ? root [0 ] : undefined
342+ })
343+
296344watch (
297345 () => route .path ,
298346 () => {
0 commit comments