Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 49 additions & 3 deletions apps/docs/.vitepress/theme/Layout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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()
Expand All @@ -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, {
Expand Down Expand Up @@ -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,
() => {
Expand Down
4 changes: 3 additions & 1 deletion apps/docs/src/components/ComponentReference.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 8 additions & 9 deletions apps/docs/src/components/PageContents.vue
Original file line number Diff line number Diff line change
@@ -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">
Expand Down
29 changes: 22 additions & 7 deletions apps/docs/src/components/PageContentsItem.vue
Original file line number Diff line number Diff line change
@@ -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>
6 changes: 6 additions & 0 deletions apps/docs/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -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}
Expand Down Expand Up @@ -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[]}