Repository: bootstrap-vue-next/bootstrap-vue-next
Pull request #2679: docs: implement on this page expand/collapse with useScrollspy
Author: login: dwgray
Created: 2025-05-01T23:19:02Z | Merged: 2025-05-11T14:42:03Z

# Describe the PR

This PR gets the on this page table of contents up to parity with the BSV version by driving the OTP off of the scrollspy data rather than the page data.
- This is a clean way to get the component reference sub-nodes, which can be important for larger component complexes like BTable
- I am using the active node to show sub-nodes in the tree only if they are part of the currently active branch, which makes it easier to see where you are in larger components and migration, where having everything expanded was unwieldy
- I can show a single node as active rather than the funky state where the parents of the active node were also being shown 

## Small replication

docs

## PR checklist

<!-- (Update "[ ]" to "[x]" to check a box) -->

**What kind of change does this PR introduce?** (check at least one)

- [ ] Bugfix :bug: - `fix(...)`
- [ ] Feature - `feat(...)`
- [ ] ARIA accessibility - `fix(...)`
- [x] Documentation update - `docs(...)`
- [ ] Other (please describe)

**The PR fulfills these requirements:**

- [x] Pull request title and all commits follow the [**Conventional Commits**](https://www.conventionalcommits.org/) convention or has an [**override**](https://github.com/googleapis/release-please#how-can-i-fix-release-notes) in this pull request body **This is very important, as the `CHANGELOG` is generated from these messages, and determines the next version type. Pull requests that do not follow conventional commits or do not have an override will be denied**


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Enhanced the "On this page" sidebar with a structured, hierarchical navigation of page headers, providing improved overview and navigation for documentation pages.
  - Sidebar now highlights the currently active section as you scroll.

- **Improvements**
  - Component references in documentation now display as anchorable headings, making it easier to link directly to specific components.
  - Sidebar navigation dynamically expands to show nested sections related to the active header.

- **Refactor**
  - Improved sidebar navigation logic for better accuracy and maintainability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/apps/docs/.vitepress/theme/Layout.vue b/apps/docs/.vitepress/theme/Layout.vue
index f01b921c4..cb85cdad1 100644
--- a/apps/docs/.vitepress/theme/Layout.vue
+++ b/apps/docs/.vitepress/theme/Layout.vue
@@ -138,7 +138,7 @@
                 header-class="pb-0 d-flex offcanvas-hidden-width"
                 body-class="py-2"
               >
-                <PageContents />
+                <PageContents :contents="contents" :active-id="activeId" />
               </BOffcanvas>
             </ClientOnly>
           </aside>
@@ -193,6 +193,7 @@ import {VPNavBarSearch} from 'vitepress/theme'
 import {appInfoKey} from './keys'
 import {useMediaQuery} from '@vueuse/core'
 import PageContents from '../../src/components/PageContents.vue'
+import {type ContentsItem, type HeaderItem} from '../../src/types'
 
 // https://vitepress.dev/reference/runtime-api#usedata
 const {page} = useData()
@@ -201,9 +202,11 @@ const route = useRoute()
 const content = useTemplateRef<ComponentPublicInstance<HTMLElement>>('_content')
 const target = useTemplateRef<ComponentPublicInstance<HTMLElement>>('_target')
 
-useScrollspy(content, target, {
-  contentQuery: ':scope > div > [id], #component-reference',
+const {current: activeId, list: items} = useScrollspy(content, target, {
+  contentQuery: ':scope > div > [id], #component-reference, .component-reference h3',
   targetQuery: ':scope [href]',
+  rootMargin: '0px 0px -25%',
+  manual: true,
 })
 
 const globalData = inject(appInfoKey, {
@@ -295,6 +298,49 @@ const set = (newValue: keyof typeof map) => {
   colorMode.value = newValue
 }
 
+const headers = computed(() =>
+  items.value.map((item) => {
+    const rawTag = item.el?.tagName?.toUpperCase() ?? ''
+    const isHeading = /^H[1-6]$/.test(rawTag)
+    const tag = isHeading ? rawTag : 'DIV'
+    const level = tag.startsWith('H') ? parseInt(tag.replace('H', '')) : 3
+    return {
+      ...item,
+      tag,
+      level,
+    } as HeaderItem
+  })
+)
+
+const contents = computed(() => {
+  const root: ContentsItem[] = []
+  const stack: ContentsItem[] = []
+
+  headers.value.forEach((header) => {
+    const item = {...header, children: [] as ContentsItem[]} as ContentsItem
+
+    while (stack.length && stack[stack.length - 1].level >= item.level) {
+      stack.pop()
+    }
+
+    if (stack.length === 0) {
+      root.push(item)
+    } else {
+      stack[stack.length - 1].children.push(item)
+    }
+
+    stack.push(item)
+  })
+
+  if (root.length !== 1) {
+    // Something isn't right if we have no root items or more than one root item
+    // eslint-disable-next-line no-console
+    console.warn('Unexpected header structure:', headers, 'Root items:', root)
+  }
+
+  return root.length > 0 ? root[0] : undefined
+})
+
 watch(
   () => route.path,
   () => {
diff --git a/apps/docs/src/components/ComponentReference.vue b/apps/docs/src/components/ComponentReference.vue
index 27c7df633..2a919f8bc 100644
--- a/apps/docs/src/components/ComponentReference.vue
+++ b/apps/docs/src/components/ComponentReference.vue
@@ -10,7 +10,9 @@
         <BContainer v-for="component in sortData" :key="component.component" fluid class="p-0">
           <BRow>
             <BCol>
-              <code class="display-6">{{ `<` + component.component + `>` }}</code>
+              <h3 :id="kebabCase(component.component)">
+                <code class="display-6">{{ `<` + component.component + `>` }}</code>
+              </h3>
             </BCol>
             <BCol v-if="globalData && component.sourcePath !== null" cols="4" class="text-md-right">
               <ViewSourceButton
diff --git a/apps/docs/src/components/PageContents.vue b/apps/docs/src/components/PageContents.vue
index d2d25b05b..4dcebce5c 100644
--- a/apps/docs/src/components/PageContents.vue
+++ b/apps/docs/src/components/PageContents.vue
@@ -1,19 +1,18 @@
 <template>
   <BNav vertical small class="otp-nav">
-    <PageContentsItem v-for="header in headers" :key="header.slug" :item="header" />
-    <BNavItem v-if="isComponentPage" href="#component-reference" link-class="otp-link"
-      >Component Reference</BNavItem
-    >
+    <PageContentsItem
+      v-for="item in contents?.children"
+      :key="item.id!"
+      :item="item"
+      :active-id="activeId"
+    />
   </BNav>
 </template>
 
 <script setup lang="ts">
-import {computed} from 'vue'
-import {useData} from 'vitepress'
+import type {ContentsItem} from 'src/types'
 
-const data = useData()
-const headers = computed(() => data.page.value.headers)
-const isComponentPage = computed(() => data.page.value.relativePath.includes('/components/'))
+defineProps<{contents?: ContentsItem; activeId: string | null}>()
 </script>
 
 <style lang="scss">
diff --git a/apps/docs/src/components/PageContentsItem.vue b/apps/docs/src/components/PageContentsItem.vue
index 0cc0c095c..b9ab264dd 100644
--- a/apps/docs/src/components/PageContentsItem.vue
+++ b/apps/docs/src/components/PageContentsItem.vue
@@ -1,18 +1,33 @@
 <template>
-  <BNavItem :href="item.link" link-class="otp-link">
-    <template #default>{{ item.title }}</template>
+  <BNavItem :href="buildLink(item)" link-class="otp-link" :active="item.id === activeId">
+    <template #default>{{ props.item.text }}</template>
     <template #after>
-      <BNav v-if="item.children?.length > 0" vertical small class="otp-nav">
-        <PageContentsItem v-for="child in item.children" :key="child.slug" :item="child" />
+      <BNav v-show="isVisible" vertical small class="otp-nav">
+        <PageContentsItem
+          v-for="child in item.children"
+          :key="child.id!"
+          :item="child"
+          :active-id="activeId"
+        />
       </BNav>
     </template>
   </BNavItem>
 </template>
 
 <script setup lang="ts">
-import {type Header} from 'vitepress'
+import type {ContentsItem} from 'src/types'
+import {computed} from 'vue'
 
-defineProps<{
-  item: Header
+const props = defineProps<{
+  item: ContentsItem
+  activeId: string | null
 }>()
+
+const buildLink = (item: ContentsItem): string => `#${item.id}`
+
+const childrenVisible = (item: ContentsItem): boolean =>
+  !!item.children?.length &&
+  item.children.some((child) => child.id === props.activeId || childrenVisible(child))
+
+const isVisible = computed(() => props.item.id === props.activeId || childrenVisible(props.item))
 </script>
diff --git a/apps/docs/src/types/index.ts b/apps/docs/src/types/index.ts
index a56d76b38..f1203bbf3 100644
--- a/apps/docs/src/types/index.ts
+++ b/apps/docs/src/types/index.ts
@@ -1,3 +1,5 @@
+import type {useScrollspy} from 'bootstrap-vue-next'
+
 export type ComponentItem = Exclude<keyof ComponentReference, 'component' | 'sections'>
 export type ComponentSection = 'Properties' | 'Events' | 'Slots'
 export type EmitArgReference = {arg: string; type: string; description?: string}
@@ -94,3 +96,7 @@ export type CollectiveMembersResponse = {
 export type CollectivePartialResponse = {
   members: CollectiveMembersResponse[]
 }
+
+export type ScrollspyItem = ReturnType<typeof useScrollspy>['list']['value'][0]
+export type HeaderItem = ScrollspyItem & {tag: string; level: number}
+export type ContentsItem = HeaderItem & {children: ContentsItem[]}


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>apps/docs/.vitepress/theme/Layout.vue (1)</summary><blockquote>

`301-311`: **Guard against non-heading tags and malformed ids**

1. `parseInt(tag.replace('H', ''))` can return `NaN` (e.g. if `tag === 'H'`).  
2. Non-heading elements will yield a default level of `3`, but their tag is still coerced to `'H0'`, which is semantically misleading.

```diff
-const tag = item.el?.tagName.toUpperCase() ?? 'H0'
-const level = tag.startsWith('H') ? parseInt(tag.replace('H', '')) : 3
+const rawTag = item.el?.tagName?.toUpperCase() ?? ''
+const isHeading = /^H[1-6]$/.test(rawTag)
+const tag = isHeading ? rawTag : 'DIV'
+const level = isHeading ? Number(rawTag.slice(1)) : 3
```

This avoids `NaN` levels and keeps the `tag` consistent with the real element.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between c1645a92d3ede914ef0fc939bed65066e13020ff and eccd099bbf5aef0ce5535f265c5b80621f8c7eaf.

</details>

<details>
<summary>📒 Files selected for processing (5)</summary>

* `apps/docs/.vitepress/theme/Layout.vue` (4 hunks)
* `apps/docs/src/components/ComponentReference.vue` (1 hunks)
* `apps/docs/src/components/PageContents.vue` (1 hunks)
* `apps/docs/src/components/PageContentsItem.vue` (1 hunks)
* `apps/docs/src/types/index.ts` (2 hunks)

</details>

<details>
<summary>🔇 Additional comments (10)</summary><blockquote>

<details>
<summary>apps/docs/src/types/index.ts (2)</summary>

`1-1`: **Import looks good**

The `useScrollspy` import provides the necessary type information for the scrollspy-related types defined later in this file.

---

`100-102`: **Well-structured type hierarchy for scrollspy navigation**

The type definitions establish a clear hierarchy:
1. `ScrollspyItem` provides the base scrollspy data structure
2. `HeaderItem` extends it with semantic information (tag and level)
3. `ContentsItem` adds recursive structure for hierarchical navigation

This approach provides good type safety while maintaining a clean separation of concerns.

</details>
<details>
<summary>apps/docs/src/components/ComponentReference.vue (1)</summary>

`13-15`: **Good semantic improvement for component references**

Wrapping the component name in an `<h3>` with a kebab-cased ID makes these references:
1. Properly structured in the document outline
2. Navigable through anchor links
3. Discoverable by the scrollspy system

This change enables component references to appear in the "On this page" navigation, improving documentation usability.

</details>
<details>
<summary>apps/docs/src/components/PageContents.vue (2)</summary>

`3-8`: **Clean prop-driven implementation**

The component now renders its children directly from the hierarchical data structure provided via props, replacing previous reliance on global state. This is a significant improvement in component design.

---

`13-15`: **Proper type definition and props interface**

The component now clearly defines its interface with proper TypeScript types:
- `contents`: Optional hierarchical structure of type `ContentsItem`
- `activeId`: Current active header ID for highlighting

This makes the component more predictable and easier to use.

</details>
<details>
<summary>apps/docs/src/components/PageContentsItem.vue (4)</summary>

`2-3`: **Improved active state handling**

The NavItem now properly:
- Builds links dynamically based on item IDs
- Sets the active state by comparing the current item ID with the active ID
- Displays the item text correctly

This creates a more responsive and accurate navigation experience.

---

`5-11`: **Smart recursive visibility logic**

Child items are now conditionally displayed based on intelligent visibility logic rather than simply checking if children exist. This implements the expand/collapse behavior requested in the PR objectives.

---

`18-24`: **Well-defined component interface**

The component now has a clear interface with proper TypeScript types:
- `item`: The navigation item of type `ContentsItem`
- `activeId`: The currently active item ID

This improves component reusability and maintainability.

---

`26-32`: **Elegant recursive visibility implementation**

The implementation includes:
- A simple `buildLink` function for consistent URL generation
- A recursive `childrenVisible` function that properly handles deeply nested structures
- A computed property that efficiently determines visibility based on the active state

This handles complex hierarchical navigation structures seamlessly.

</details>
<details>
<summary>apps/docs/.vitepress/theme/Layout.vue (1)</summary>

`196-210`: **Verify `useScrollspy` options – `manual: true` requires an explicit `update()`**

`manual: true` disables the library’s internal `IntersectionObserver` updates; you must now call `items.update()` (or the exposed `update()` function) whenever the DOM changes or on route navigation, otherwise the list will be frozen on the first render.

If you intended the default behaviour, simply remove the flag.

```diff
-const {current: currentItem, list: items} = useScrollspy(content, target, {
+const {current: currentItem, list: items} = useScrollspy(content, target, {
   contentQuery: ':scope > div > [id], #component-reference, .component-reference h3',
   targetQuery: ':scope [href]',
   rootMargin: '0px 0px -25%',
-  manual: true,
+  // manual: true, // ← uncomment only if you call items.update() yourself
 })
```

If you keep `manual: true`, remember to invoke `items.update()` inside a `watchEffect` on `route.path` and after the page content finishes rendering.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T23:31:42Z
user: login: dwgray
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-01T23:40:19Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T23:40:58Z
user: login: dwgray
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-01T23:42:13Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T23:42:35Z
user: login: VividLemon
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-07T16:15:07Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>apps/docs/src/utils/filter-active-id.ts (3)</summary><blockquote>

`11-18`: **Consider adding a null check for item parameter**

While the code handles null `activeId` values, it might be worth adding a guard clause for when `item` is null/undefined to make the function more robust against potential runtime errors.

```diff
export const filteredActiveId = (item: ContentsItem, activeId: string | null): string | null => {
+  if (item === null || item === undefined) {
+    return null;
+  }
  if (activeId === null) {
    return null
  }
  // rest of code...
}
```

---

`1-1`: **Consider using a more specific import path**

The import path 'src/types' might work in the current context, but for better maintainability, consider using a relative path like '../types' or an absolute path that follows the project's convention.

---

`4-4`: **Consider a more descriptive function name**

While `filteredActiveId` works, a more descriptive name like `findActiveIdInSubtree` or `isActiveIdInSubtree` might better communicate the function's purpose to future developers.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between d82136a85f68c19166baa1999f890f55ca558287 and 64f29c2c5111045974c7e1689737c66a7506bd15.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `apps/docs/src/components/PageContents.vue` (1 hunks)
* `apps/docs/src/components/PageContentsItem.vue` (1 hunks)
* `apps/docs/src/utils/filter-active-id.ts` (1 hunks)
* `apps/docs/src/utils/index.ts` (1 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* apps/docs/src/utils/index.ts

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary>

* apps/docs/src/components/PageContentsItem.vue
* apps/docs/src/components/PageContents.vue

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>apps/docs/src/utils/filter-active-id.ts (2)</summary><blockquote>

<details>
<summary>apps/docs/src/utils/index.ts (1)</summary>

* `filteredActiveId` (8-8)

</details>
<details>
<summary>apps/docs/src/types/index.ts (1)</summary>

* `ContentsItem` (102-102)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: build

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>apps/docs/src/utils/filter-active-id.ts (1)</summary>

`3-20`: **Well-implemented recursive utility function!**

This function elegantly traverses the navigation tree to determine if the active ID exists within a subtree. The implementation uses clear recursion with appropriate early returns for handling edge cases.

This approach effectively supports the expand/collapse functionality in the documentation's "on this page" table of contents by determining which branches contain the active section.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T18:08:02Z
user: login: dwgray
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T18:11:43Z
user: login: VividLemon
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-10T18:34:31Z
user: login: dwgray
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-10T20:26:17Z

Review comments:
diff_hunk: @@ -138,7 +138,7 @@
                 header-class="pb-0 d-flex offcanvas-hidden-width"
                 body-class="py-2"
               >
-                <PageContents />
+                <PageContents :contents="contents" :active-id="currentItem" />
               </BOffcanvas>
path: apps/docs/.vitepress/theme/Layout.vue
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

_⚠️ Potential issue_

**Pass the actual header *id* instead of the full Scrollspy item**

`currentItem` is a ref that holds the *whole* scroll-spy record (or `null`).  
`PageContents` only needs the identifier (`string | undefined`) so it can compare it with the ids in the tree.  
Passing the entire object will force `PageContents` to perform `activeId?.id ?? activeId` style checks – or, if it assumes a string, it will silently fail to highlight anything.

```diff
-<PageContents :contents="contents" :active-id="currentItem" />
+<PageContents :contents="contents" :active-id="currentItem?.id" />
```

(The template ref-unwrapping still returns the object; we explicitly pick the `id`.)





<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
                <PageContents :contents="contents" :active-id="currentItem?.id" />
              </BOffcanvas>
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-01T23:31:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -295,6 +298,47 @@ const set = (newValue: keyof typeof map) => {
   colorMode.value = newValue
 }
 
+const headers = computed(() =>
+  items.value.map((item) => {
+    const tag = item.el?.tagName.toUpperCase() ?? 'H0'
+    const level = tag.startsWith('H') ? parseInt(tag.replace('H', '')) : 3
+    return {
+      ...item,
+      tag,
+      level,
+    } as HeaderItem
+  })
+)
+
+const contents = computed(() => {
+  const root: ContentsItem[] = []
+  const stack: ContentsItem[] = []
+
+  headers.value.forEach((header) => {
+    const item = {...header, children: [] as ContentsItem[]} as ContentsItem
+
+    while (stack.length && stack[stack.length - 1].level >= item.level) {
+      stack.pop()
+    }
+
+    if (stack.length === 0) {
+      root.push(item)
+    } else {
+      stack[stack.length - 1].children.push(item)
+    }
+
+    stack.push(item)
+  })
+
+  if (root.length !== 1) {
+    // Something isn't right if we have no root items or more than one root item
+    // eslint-disable-next-line no-console
+    console.warn('Unexpected header structure:', headers, 'Root items:', root)
+  }
+
+  return root.length > 0 ? root[0] : undefined
+})
path: apps/docs/.vitepress/theme/Layout.vue
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

_⚠️ Potential issue_

**The tree builder drops every root-level heading after the first one**

`root.push(item)` collects *all* top-level headings, yet the function returns only `root[0]`.  
Pages that start directly with multiple `H2`s (many docs do) will therefore show **only the first section** in the OTP.

Either:

1. Treat `root` as the complete top level and return it (simplest):

```diff
-return root.length > 0 ? root[0] : undefined
+return root
```

…and adjust `PageContents`’ prop type to accept `ContentsItem[]`.

2. Introduce a synthetic “root” node that wraps `root` (mirroring how VuePress does it).

Until one of these is implemented the sidebar will appear empty for any document that lacks a single unique parent header.





<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
const contents = computed(() => {
  const root: ContentsItem[] = []
  const stack: ContentsItem[] = []

  headers.value.forEach((header) => {
    const item = { ...header, children: [] as ContentsItem[] } as ContentsItem

    while (stack.length && stack[stack.length - 1].level >= item.level) {
      stack.pop()
    }

    if (stack.length === 0) {
      root.push(item)
    } else {
      stack[stack.length - 1].children.push(item)
    }

    stack.push(item)
  })

  if (root.length !== 1) {
    // Something isn't right if we have no root items or more than one root item
    // eslint-disable-next-line no-console
    console.warn('Unexpected header structure:', headers, 'Root items:', root)
  }

  return root
})
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-01T23:31:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -138,7 +138,7 @@
                 header-class="pb-0 d-flex offcanvas-hidden-width"
                 body-class="py-2"
               >
-                <PageContents />
+                <PageContents :contents="contents" :active-id="currentItem" />
               </BOffcanvas>
path: apps/docs/.vitepress/theme/Layout.vue
user: login: dwgray
body: currentItem is the variable returned from useScrollSpy as current. This is actually the id of the currently active item - a string | null. I'm renaming the variable to avoid this confusion, but not accepting the suggested change, since it wouldn't work - you can't take the id field of a string | null.
created_at: 2025-05-01T23:40:19Z
author_association: MEMBER
diff_hunk: @@ -138,7 +138,7 @@
                 header-class="pb-0 d-flex offcanvas-hidden-width"
                 body-class="py-2"
               >
-                <PageContents />
+                <PageContents :contents="contents" :active-id="currentItem" />
               </BOffcanvas>
path: apps/docs/.vitepress/theme/Layout.vue
user: login: coderabbitai[bot]
body: @dwgray You're absolutely right! If `currentItem` is already a string or null (the ID itself), then trying to access `.id` on it would not work. Thank you for clarifying.

I see the script uses `activeId` (from destructuring `current` in useScrollspy), while the template uses `currentItem`. Renaming for consistency is definitely a good approach to avoid confusion.

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: dwgray
PR: bootstrap-vue-next/bootstrap-vue-next#2679
File: apps/docs/.vitepress/theme/Layout.vue:0-0
Timestamp: 2025-05-01T23:40:56.131Z
Learning: In bootstrap-vue-next, the `useScrollspy` hook returns an object with a `current` property which is the ID string (or null) of the currently active item, not an object containing an ID.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>




<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@dwgray You're absolutely right! If `currentItem` is already a string or null (the ID itself), then trying to access `.id` on it would not work. Thank you for clarifying.

I see the script uses `activeId` (from destructuring `current` in useScrollspy), while the template uses `currentItem`. Renaming for consistency is definitely a good approach to avoid confusion.

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: dwgray
PR: bootstrap-vue-next/bootstrap-vue-next#2679
File: apps/docs/.vitepress/theme/Layout.vue:0-0
Timestamp: 2025-05-01T23:40:56.131Z
Learning: In bootstrap-vue-next, the `useScrollspy` hook returns an object with a `current` property which is the ID string (or null) of the currently active item, not an object containing an ID.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-01T23:40:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -295,6 +298,47 @@ const set = (newValue: keyof typeof map) => {
   colorMode.value = newValue
 }
 
+const headers = computed(() =>
+  items.value.map((item) => {
+    const tag = item.el?.tagName.toUpperCase() ?? 'H0'
+    const level = tag.startsWith('H') ? parseInt(tag.replace('H', '')) : 3
+    return {
+      ...item,
+      tag,
+      level,
+    } as HeaderItem
+  })
+)
+
+const contents = computed(() => {
+  const root: ContentsItem[] = []
+  const stack: ContentsItem[] = []
+
+  headers.value.forEach((header) => {
+    const item = {...header, children: [] as ContentsItem[]} as ContentsItem
+
+    while (stack.length && stack[stack.length - 1].level >= item.level) {
+      stack.pop()
+    }
+
+    if (stack.length === 0) {
+      root.push(item)
+    } else {
+      stack[stack.length - 1].children.push(item)
+    }
+
+    stack.push(item)
+  })
+
+  if (root.length !== 1) {
+    // Something isn't right if we have no root items or more than one root item
+    // eslint-disable-next-line no-console
+    console.warn('Unexpected header structure:', headers, 'Root items:', root)
+  }
+
+  return root.length > 0 ? root[0] : undefined
+})
path: apps/docs/.vitepress/theme/Layout.vue
user: login: dwgray
body: This code runs in a controlled context where there will always be a single root and there will be other issues if there are multiple roots, emitting a warning if the content is malformed seems reasonable here.
created_at: 2025-05-01T23:42:13Z
author_association: MEMBER
diff_hunk: @@ -295,6 +298,47 @@ const set = (newValue: keyof typeof map) => {
   colorMode.value = newValue
 }
 
+const headers = computed(() =>
+  items.value.map((item) => {
+    const tag = item.el?.tagName.toUpperCase() ?? 'H0'
+    const level = tag.startsWith('H') ? parseInt(tag.replace('H', '')) : 3
+    return {
+      ...item,
+      tag,
+      level,
+    } as HeaderItem
+  })
+)
+
+const contents = computed(() => {
+  const root: ContentsItem[] = []
+  const stack: ContentsItem[] = []
+
+  headers.value.forEach((header) => {
+    const item = {...header, children: [] as ContentsItem[]} as ContentsItem
+
+    while (stack.length && stack[stack.length - 1].level >= item.level) {
+      stack.pop()
+    }
+
+    if (stack.length === 0) {
+      root.push(item)
+    } else {
+      stack[stack.length - 1].children.push(item)
+    }
+
+    stack.push(item)
+  })
+
+  if (root.length !== 1) {
+    // Something isn't right if we have no root items or more than one root item
+    // eslint-disable-next-line no-console
+    console.warn('Unexpected header structure:', headers, 'Root items:', root)
+  }
+
+  return root.length > 0 ? root[0] : undefined
+})
path: apps/docs/.vitepress/theme/Layout.vue
user: login: coderabbitai[bot]
body: @dwgray I understand your reasoning. If the code is running in a controlled context where you can guarantee there will always be a single root element, then the current implementation with the warning is appropriate. The warning serves as a good safeguard in case something unexpected happens while maintaining the expected return type. Thanks for explaining your design decision.


<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@dwgray I understand your reasoning. If the code is running in a controlled context where you can guarantee there will always be a single root element, then the current implementation with the warning is appropriate. The warning serves as a good safeguard in case something unexpected happens while maintaining the expected return type. Thanks for explaining your design decision.</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-01T23:42:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,18 +1,33 @@
 <template>
-  <BNavItem :href="item.link" link-class="otp-link">
-    <template #default>{{ item.title }}</template>
+  <BNavItem :href="buildLink(item)" link-class="otp-link" :active="item.id === activeId">
+    <template #default>{{ props.item.text }}</template>
     <template #after>
-      <BNav v-if="item.children?.length > 0" vertical small class="otp-nav">
-        <PageContentsItem v-for="child in item.children" :key="child.slug" :item="child" />
+      <BNav v-show="isVisible" vertical small class="otp-nav">
+        <PageContentsItem
+          v-for="child in item.children"
+          :key="child.id!"
+          :item="child"
+          :active-id="activeId"
path: apps/docs/src/components/PageContentsItem.vue
user: login: VividLemon
body: Somewhere along the lines here you made a https://vuejs.org/guide/best-practices/performance#props-stability issue
created_at: 2025-05-07T16:15:01Z
author_association: MEMBER
diff_hunk: @@ -1,18 +1,33 @@
 <template>
-  <BNavItem :href="item.link" link-class="otp-link">
-    <template #default>{{ item.title }}</template>
+  <BNavItem :href="buildLink(item)" link-class="otp-link" :active="item.id === activeId">
+    <template #default>{{ props.item.text }}</template>
     <template #after>
-      <BNav v-if="item.children?.length > 0" vertical small class="otp-nav">
-        <PageContentsItem v-for="child in item.children" :key="child.slug" :item="child" />
+      <BNav v-show="isVisible" vertical small class="otp-nav">
+        <PageContentsItem
+          v-for="child in item.children"
+          :key="child.id!"
+          :item="child"
+          :active-id="activeId"
path: apps/docs/src/components/PageContentsItem.vue
user: login: dwgray
body: Interesting - this is not quite as easily fixed as the vue example shows, since PageContentsItems is a recursive component - I can't just pass in a boolean. The solution I pushed was to check if the currently active id is in the subtree for the component and pass in null if it isn't. That maintains prop stability for everything except for the subtree that is activating and the one that is deactivating at the cost of extra complexity and computation in our code.

I'm not sure that's the `right` solution here, as I don't have a strong gut on the performance characteristics. In other contexts, I would run a profiler on the code before and after to get that sense. @VividLemon, do you have a gut sense of where you'd take the tradeoffs here?
created_at: 2025-05-07T18:11:43Z
author_association: MEMBER
diff_hunk: @@ -1,18 +1,33 @@
 <template>
-  <BNavItem :href="item.link" link-class="otp-link">
-    <template #default>{{ item.title }}</template>
+  <BNavItem :href="buildLink(item)" link-class="otp-link" :active="item.id === activeId">
+    <template #default>{{ props.item.text }}</template>
     <template #after>
-      <BNav v-if="item.children?.length > 0" vertical small class="otp-nav">
-        <PageContentsItem v-for="child in item.children" :key="child.slug" :item="child" />
+      <BNav v-show="isVisible" vertical small class="otp-nav">
+        <PageContentsItem
+          v-for="child in item.children"
+          :key="child.id!"
+          :item="child"
+          :active-id="activeId"
path: apps/docs/src/components/PageContentsItem.vue
user: login: VividLemon
body: It could just be larger benefit to revert the changes for simplicity. It's a minor perf change. The prop passing down twice is kinda complicating things as I think the parent of the vfor is going to rerender anyways 
created_at: 2025-05-10T18:34:31Z
author_association: MEMBER
diff_hunk: @@ -1,18 +1,33 @@
 <template>
-  <BNavItem :href="item.link" link-class="otp-link">
-    <template #default>{{ item.title }}</template>
+  <BNavItem :href="buildLink(item)" link-class="otp-link" :active="item.id === activeId">
+    <template #default>{{ props.item.text }}</template>
     <template #after>
-      <BNav v-if="item.children?.length > 0" vertical small class="otp-nav">
-        <PageContentsItem v-for="child in item.children" :key="child.slug" :item="child" />
+      <BNav v-show="isVisible" vertical small class="otp-nav">
+        <PageContentsItem
+          v-for="child in item.children"
+          :key="child.id!"
+          :item="child"
+          :active-id="activeId"
path: apps/docs/src/components/PageContentsItem.vue
user: login: dwgray
body: Thank - That's where my intuition goes as well, but it was interesting to go down the path of keeping the props stable. I've reverted the change.
created_at: 2025-05-10T20:26:16Z
author_association: MEMBER

PR comments:
user: login: bolt-new-by-stackblitz[bot]
created_at: 2025-05-01T23:19:04Z
body: <a href="https://stackblitz.com/~/github.com/bootstrap-vue-next/bootstrap-vue-next/pull/2679"><img src="https://developer.stackblitz.com/img/review_pr_small.svg" alt="Review PR in StackBlitz Codeflow" align="left" width="103" height="20"></a> _Run & review this pull request in [StackBlitz Codeflow](https://stackblitz.com/~/github.com/bootstrap-vue-next/bootstrap-vue-next/pull/2679)._

author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-05-01T23:19:08Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

The updates refactor the documentation site's sidebar navigation by introducing hierarchical header structures and explicit active state tracking. New types are defined to support these structures. The sidebar now receives structured header data and the active header ID as props, enabling improved rendering and navigation for nested content, including component references.

## Changes

| Files                                                                                       | Change Summary |
|---------------------------------------------------------------------------------------------|---------------|
| apps/docs/.vitepress/theme/Layout.vue                                                       | Enhanced sidebar logic to compute and pass hierarchical header structures (`contents`) and the current active header ID (`active-id`) to `PageContents`. Updated scrollspy logic to support these features. |
| apps/docs/src/components/ComponentReference.vue                                             | Changed component name rendering from a `<code>` element to an `<h3>` with an anchorable ID for semantic structure and navigation. |
| apps/docs/src/components/PageContents.vue                                                   | Refactored to accept `contents` and `activeId` as props, removing internal page data dependencies and conditional logic. Now renders navigation based on passed-in hierarchical data. |
| apps/docs/src/components/PageContentsItem.vue                                               | Updated to use `ContentsItem` type, added `activeId` prop, and implemented recursive logic for active state and nested navigation visibility. |
| apps/docs/src/types/index.ts                                                                | Added new type definitions (`ScrollspyItem`, `HeaderItem`, `ContentsItem`) to support hierarchical, typed sidebar content structures. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant Layout
    participant useScrollspy
    participant PageContents
    participant PageContentsItem

    Layout->>useScrollspy: Get headers and activeId
    Layout->>Layout: Build hierarchical contents structure
    Layout->>PageContents: Pass contents and activeId as props
    PageContents->>PageContentsItem: Render items recursively with activeId
    PageContentsItem->>PageContentsItem: Check active state and visibility for children
```

## Poem

> 🐇  
> In the docs a sidebar grew,  
> With headers nested, clear and true.  
> Active links now light the way,  
> Through components on display.  
> Types and props now guide the dance—  
> Hop along, enhance your glance!  
>

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!TIP]
> <details>
> <summary>⚡️ Faster reviews with caching</summary>
> 
> - CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure `Review - Disable Cache` at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the `Data Retention` setting under your Organization Settings.
> 
> Enjoy the performance boost—your workflow just got faster.
> 
> </details>

<!-- announcements_end -->

---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 64f29c2c5111045974c7e1689737c66a7506bd15 and 8f3d3a29327dac36ff632185ad492607f6d1647d.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `apps/docs/src/components/PageContents.vue` (1 hunks)
* `apps/docs/src/components/PageContentsItem.vue` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary>

* apps/docs/src/components/PageContents.vue
* apps/docs/src/components/PageContentsItem.vue

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: build

</details>

</details>
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=bootstrap-vue-next/bootstrap-vue-next&utm_content=2679):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: pkg-pr-new[bot]
created_at: 2025-05-01T23:21:25Z
body: 
[bsvn-vite-ts](https://pkg.pr.new/template/233d7fa3-4cda-4749-b991-930847a60f49)


```
npm i https://pkg.pr.new/bootstrap-vue-next/bootstrap-vue-next@2679
```


```
npm i https://pkg.pr.new/bootstrap-vue-next/bootstrap-vue-next/@bootstrap-vue-next/nuxt@2679
```


_commit: <a href="https://github.com/bootstrap-vue-next/bootstrap-vue-next/runs/41995721193"><code>8f3d3a2</code></a>_

author_association: NONE