mirror of
https://github.com/pengzhanbo/vuepress-theme-plume.git
synced 2026-04-23 10:58:13 +08:00
feat(theme): 新增自定义有侧边栏层级配置
This commit is contained in:
parent
131be4424f
commit
90f5409642
@ -54,7 +54,7 @@ permalink: /config/frontmatter/basic/
|
||||
- 类型: `boolean`
|
||||
- 默认值: `true`
|
||||
|
||||
当前文章内的 外部链接是否显示 外部链接图标, 即 “ <ExternalLinkIcon /> ” 图标
|
||||
当前文章内的 外部链接是否显示 外部链接图标。
|
||||
|
||||
### backToTop
|
||||
|
||||
@ -77,6 +77,20 @@ permalink: /config/frontmatter/basic/
|
||||
|
||||
当前文章是否 显示 右侧边栏。
|
||||
|
||||
### outline
|
||||
|
||||
- 类型: `false | number | [number, number] | 'deep'`
|
||||
- 默认值: `[2, 3]`
|
||||
- 详情:
|
||||
|
||||
要显示的标题级别。
|
||||
|
||||
单个数字表示只显示该级别的标题。
|
||||
|
||||
如果传递的是一个元组,第一个数字是最小级别,第二个数字是最大级别。
|
||||
|
||||
`'deep'` 与 `[2, 6]` 相同,将显示从 `<h2>` 到 `<h6>` 的所有标题。
|
||||
|
||||
### prev
|
||||
|
||||
- 类型: `string | { text: string, link: string, icon?: string }`
|
||||
|
||||
@ -16,6 +16,79 @@ permalink: /config/basic/
|
||||
主题使用的插件默认已进行了配置,大多数情况下您不需要进行任何修改,如果需要使用到细致的定制化,请查阅
|
||||
[此文档](/config/plugins/)
|
||||
|
||||
### hostname
|
||||
|
||||
- 类型: `string`
|
||||
- 默认值: `''`
|
||||
- 详情:
|
||||
|
||||
部署站点域名。
|
||||
|
||||
当 `hostname` 配置为有效域名时,主题将会生成 `sitemap` 和 `seo` 相关的内容。
|
||||
|
||||
### blog
|
||||
|
||||
- 类型: `false | BlogOptions`
|
||||
- 默认值: `{ link: '/blog/', include: ['**/*.md'], exclude: [] }`
|
||||
- 详情:
|
||||
|
||||
博客配置。
|
||||
|
||||
```ts
|
||||
interface BlogOptions {
|
||||
/**
|
||||
* blog list link
|
||||
*
|
||||
* @default '/blog/'
|
||||
*/
|
||||
link?: string
|
||||
|
||||
/**
|
||||
* 在 `blog.dir` 目录中,通过 glob string 配置包含文件
|
||||
*
|
||||
* @default - ['**\*.md']
|
||||
*/
|
||||
include?: string[]
|
||||
|
||||
/**
|
||||
* 在 `blog.dir` 目录中,通过 glob string 配置排除的文件
|
||||
*
|
||||
* README.md 文件一般作为主页或者某个目录下的主页,不应该被读取为 blog文章
|
||||
*
|
||||
* @default - ['.vuepress/', 'node_modules/', '{README,index}.md']
|
||||
*/
|
||||
exclude?: string[]
|
||||
|
||||
/**
|
||||
* 分页配置
|
||||
*/
|
||||
pagination?: false | {
|
||||
/**
|
||||
* 每页显示的文章数量
|
||||
* @default 10
|
||||
*/
|
||||
perPage?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否启用标签页
|
||||
* @default true
|
||||
*/
|
||||
tags?: boolean
|
||||
/**
|
||||
* 是否启用归档页
|
||||
* @default true
|
||||
*/
|
||||
archives?: boolean
|
||||
}
|
||||
```
|
||||
|
||||
### article
|
||||
|
||||
- 类型: `string`
|
||||
- 默认值: `/article/`
|
||||
- 详情: 文章链接前缀
|
||||
|
||||
### locales
|
||||
|
||||
- 类型: `Record<string, PlumeThemeLocaleConfig>`
|
||||
@ -69,16 +142,6 @@ permalink: /config/basic/
|
||||
- 默认值: `'Appearance'`
|
||||
- 详情: 导航栏中的主题切换按钮的文本。
|
||||
|
||||
### hostname
|
||||
|
||||
- 类型: `string`
|
||||
- 默认值: `''`
|
||||
- 详情:
|
||||
|
||||
部署站点域名。
|
||||
|
||||
当 `hostname` 配置为有效域名时,主题将会生成 `sitemap` 和 `seo` 相关的内容。
|
||||
|
||||
### avatar
|
||||
|
||||
- 类型: `PlumeThemeAvatar`
|
||||
@ -162,79 +225,6 @@ export default {
|
||||
允许显示在导航栏的社交链接。
|
||||
该配置仅在 PC 端下有效。
|
||||
|
||||
### blog
|
||||
|
||||
- 类型: `false | BlogOptions`
|
||||
- 默认值: `{ link: '/blog/', include: ['**/*.md'], exclude: [] }`
|
||||
- 详情:
|
||||
|
||||
博客配置。
|
||||
|
||||
```ts
|
||||
interface BlogOptions {
|
||||
/**
|
||||
* blog list link
|
||||
*
|
||||
* @default '/blog/'
|
||||
*/
|
||||
link?: string
|
||||
|
||||
/**
|
||||
* 在 `blog.dir` 目录中,通过 glob string 配置包含文件
|
||||
*
|
||||
* @default - ['**\*.md']
|
||||
*/
|
||||
include?: string[]
|
||||
|
||||
/**
|
||||
* 在 `blog.dir` 目录中,通过 glob string 配置排除的文件
|
||||
*
|
||||
* README.md 文件一般作为主页或者某个目录下的主页,不应该被读取为 blog文章
|
||||
*
|
||||
* @default - ['.vuepress/', 'node_modules/', '{README,index}.md']
|
||||
*/
|
||||
exclude?: string[]
|
||||
|
||||
/**
|
||||
* 分页配置
|
||||
*/
|
||||
pagination?: false | {
|
||||
/**
|
||||
* 每页显示的文章数量
|
||||
* @default 10
|
||||
*/
|
||||
perPage?: number
|
||||
/**
|
||||
* 前一页的文本
|
||||
* @default 'Prev'
|
||||
*/
|
||||
prevPageText?: string
|
||||
/**
|
||||
* 后一页的文本
|
||||
* @default 'Next'
|
||||
*/
|
||||
nextPageText?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否启用标签页
|
||||
* @default true
|
||||
*/
|
||||
tags?: boolean
|
||||
/**
|
||||
* 是否启用归档页
|
||||
* @default true
|
||||
*/
|
||||
archives?: boolean
|
||||
}
|
||||
```
|
||||
|
||||
### article
|
||||
|
||||
- 类型: `string`
|
||||
- 默认值: `/article/`
|
||||
- 详情: 文章链接前缀
|
||||
|
||||
### navbar
|
||||
|
||||
- 类型: `NavItem[]`
|
||||
@ -332,12 +322,30 @@ type NavItem = string | {
|
||||
|
||||
- 类型: `false | PlumeThemeNotesOptions`
|
||||
- 默认值: `{ link: '/note', dir: 'notes', notes: [] }`
|
||||
- 详情: 笔记配置, 笔记中的文章默认不会出现在首页文章列表
|
||||
- 详情:
|
||||
|
||||
笔记配置, 笔记中的文章默认不会出现在首页文章列表
|
||||
|
||||
你可以将配置的notes 配置到 navbar中,以便浏览查看
|
||||
|
||||
详细配置请查看 [此文档](/config/notes/)
|
||||
|
||||
### outline
|
||||
|
||||
- 类型: `false | number | [number, number] | 'deep'`
|
||||
- 默认值: `[2, 3]`
|
||||
- 详情:
|
||||
|
||||
要显示的标题级别。
|
||||
|
||||
单个数字表示只显示该级别的标题。
|
||||
|
||||
如果传递的是一个元组,第一个数字是最小级别,第二个数字是最大级别。
|
||||
|
||||
`'deep'` 与 `[2, 6]` 相同,将显示从 `<h2>` 到 `<h6>` 的所有标题。
|
||||
|
||||
每个页面可以通过 [frontmatter outline](./frontmatter/basic.md#outline) 覆盖层级配置。
|
||||
|
||||
### selectLanguageName
|
||||
|
||||
- 类型: `string`
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { onContentUpdated } from '@vuepress-plume/plugin-content-update/client'
|
||||
import { useActiveAnchor, useData } from '../composables/index.js'
|
||||
import { type MenuItem, getHeaders, useActiveAnchor, useData } from '../composables/index.js'
|
||||
import PageAsideItem from './PageAsideItem.vue'
|
||||
|
||||
const { page, theme } = useData()
|
||||
const { theme, frontmatter } = useData()
|
||||
|
||||
const headers = ref(page.value.headers)
|
||||
const headers = ref<MenuItem[]>([])
|
||||
const hasOutline = computed(() => headers.value.length > 0)
|
||||
|
||||
onContentUpdated(() => {
|
||||
headers.value = page.value.headers
|
||||
headers.value = getHeaders(frontmatter.value.outline ?? theme.value.outline)
|
||||
})
|
||||
|
||||
const container = ref()
|
||||
@ -84,6 +84,7 @@ function handlePrint() {
|
||||
width: 2px;
|
||||
height: 18px;
|
||||
background-color: var(--vp-c-brand-1);
|
||||
border-radius: 2px;
|
||||
opacity: 0;
|
||||
transition:
|
||||
top 0.25s cubic-bezier(0, 1, 0.5, 1),
|
||||
@ -94,9 +95,9 @@ function handlePrint() {
|
||||
.outline-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 28px;
|
||||
line-height: 32px;
|
||||
letter-spacing: 0.4px;
|
||||
}
|
||||
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import type { PageHeader } from 'vuepress/client'
|
||||
import type { MenuItem } from '../composables/index.js'
|
||||
|
||||
defineProps<{
|
||||
headers: PageHeader[]
|
||||
headers: MenuItem[]
|
||||
root?: boolean
|
||||
}>()
|
||||
|
||||
@ -39,7 +39,9 @@ function handleClick({ target: el }: Event) {
|
||||
.outline-link {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
line-height: 28px;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 32px;
|
||||
color: var(--vp-c-text-2);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
@ -1,11 +1,7 @@
|
||||
import { useMediaQuery } from '@vueuse/core'
|
||||
import type { Ref } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, onUpdated } from 'vue'
|
||||
import { throttleAndDebounce } from '../utils/index.js'
|
||||
import { computed } from 'vue'
|
||||
import { useSidebar } from './sidebar.js'
|
||||
|
||||
const PAGE_OFFSET = 71
|
||||
|
||||
export function useAside() {
|
||||
const { hasSidebar } = useSidebar()
|
||||
const is960 = useMediaQuery('(min-width: 960px)')
|
||||
@ -22,114 +18,3 @@ export function useAside() {
|
||||
isAsideEnabled,
|
||||
}
|
||||
}
|
||||
|
||||
export function useActiveAnchor(
|
||||
container: Ref<HTMLElement>,
|
||||
marker: Ref<HTMLElement>,
|
||||
) {
|
||||
const { isAsideEnabled } = useAside()
|
||||
|
||||
const onScroll = throttleAndDebounce(setActiveLink, 100)
|
||||
|
||||
let prevActiveLink: HTMLAnchorElement | null = null
|
||||
|
||||
onMounted(() => {
|
||||
requestAnimationFrame(setActiveLink)
|
||||
window.addEventListener('scroll', onScroll)
|
||||
})
|
||||
|
||||
onUpdated(() => {
|
||||
// sidebar update means a route change
|
||||
activateLink(location.hash)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('scroll', onScroll)
|
||||
})
|
||||
|
||||
function setActiveLink() {
|
||||
if (!isAsideEnabled.value)
|
||||
return
|
||||
|
||||
const links = [].slice.call(
|
||||
container.value.querySelectorAll('.outline-link'),
|
||||
) as HTMLAnchorElement[]
|
||||
|
||||
const anchors = [].slice
|
||||
.call(document.querySelectorAll('.content .header-anchor'))
|
||||
.filter((anchor: HTMLAnchorElement) => {
|
||||
return links.some((link) => {
|
||||
return link.hash === anchor.hash && anchor.offsetParent !== null
|
||||
})
|
||||
}) as HTMLAnchorElement[]
|
||||
|
||||
const scrollY = window.scrollY
|
||||
const innerHeight = window.innerHeight
|
||||
const offsetHeight = document.body.offsetHeight
|
||||
const isBottom = Math.abs(scrollY + innerHeight - offsetHeight) < 1
|
||||
|
||||
// page bottom - highlight last one
|
||||
if (anchors.length && isBottom) {
|
||||
activateLink(anchors[anchors.length - 1].hash)
|
||||
return
|
||||
}
|
||||
|
||||
for (let i = 0; i < anchors.length; i++) {
|
||||
const anchor = anchors[i]
|
||||
const nextAnchor = anchors[i + 1]
|
||||
|
||||
const [isActive, hash] = isAnchorActive(i, anchor, nextAnchor)
|
||||
|
||||
if (isActive) {
|
||||
activateLink(hash)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function activateLink(hash: string | null) {
|
||||
if (prevActiveLink)
|
||||
prevActiveLink.classList.remove('active')
|
||||
|
||||
if (hash !== null) {
|
||||
prevActiveLink = container.value.querySelector(
|
||||
`a[href="${decodeURIComponent(hash)}"]`,
|
||||
)
|
||||
}
|
||||
|
||||
const activeLink = prevActiveLink
|
||||
|
||||
if (activeLink) {
|
||||
activeLink.classList.add('active')
|
||||
marker.value.style.top = `${activeLink.offsetTop + 33}px`
|
||||
marker.value.style.opacity = '1'
|
||||
}
|
||||
else {
|
||||
marker.value.style.top = '33px'
|
||||
marker.value.style.opacity = '0'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getAnchorTop(anchor: HTMLAnchorElement): number {
|
||||
return anchor.parentElement!.offsetTop - PAGE_OFFSET
|
||||
}
|
||||
|
||||
function isAnchorActive(
|
||||
index: number,
|
||||
anchor: HTMLAnchorElement,
|
||||
nextAnchor: HTMLAnchorElement | undefined,
|
||||
): [boolean, string | null] {
|
||||
const scrollTop = window.scrollY
|
||||
|
||||
if (index === 0 && scrollTop === 0)
|
||||
return [true, null]
|
||||
|
||||
if (scrollTop < getAnchorTop(anchor))
|
||||
return [false, null]
|
||||
|
||||
if (!nextAnchor || scrollTop < getAnchorTop(nextAnchor))
|
||||
return [true, anchor.hash]
|
||||
|
||||
return [false, null]
|
||||
}
|
||||
|
||||
@ -10,3 +10,4 @@ export * from './locale.js'
|
||||
export * from './useRouteQuery.js'
|
||||
export * from './watermark.js'
|
||||
export * from './data.js'
|
||||
export * from './outline.js'
|
||||
|
||||
243
theme/src/client/composables/outline.ts
Normal file
243
theme/src/client/composables/outline.ts
Normal file
@ -0,0 +1,243 @@
|
||||
import { onMounted, onUnmounted, onUpdated } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
import type { PlumeThemeLocaleData } from '../../shared/index.js'
|
||||
import { throttleAndDebounce } from '../utils/index.js'
|
||||
import { useAside } from './aside.js'
|
||||
|
||||
export interface Header {
|
||||
/**
|
||||
* The level of the header
|
||||
*
|
||||
* `1` to `6` for `<h1>` to `<h6>`
|
||||
*/
|
||||
level: number
|
||||
/**
|
||||
* The title of the header
|
||||
*/
|
||||
title: string
|
||||
/**
|
||||
* The slug of the header
|
||||
*
|
||||
* Typically the `id` attr of the header anchor
|
||||
*/
|
||||
slug: string
|
||||
/**
|
||||
* Link of the header
|
||||
*
|
||||
* Typically using `#${slug}` as the anchor hash
|
||||
*/
|
||||
link: string
|
||||
/**
|
||||
* The children of the header
|
||||
*/
|
||||
children: Header[]
|
||||
}
|
||||
|
||||
// cached list of anchor elements from resolveHeaders
|
||||
const resolvedHeaders: { element: HTMLHeadElement, link: string }[] = []
|
||||
|
||||
export type MenuItem = Omit<Header, 'slug' | 'children'> & {
|
||||
element: HTMLHeadElement
|
||||
children?: MenuItem[]
|
||||
}
|
||||
|
||||
export function getHeaders(range: PlumeThemeLocaleData['outline']): MenuItem[] {
|
||||
const headers = Array.from(
|
||||
document.querySelectorAll('.plume-content :where(h1,h2,h3,h4,h5,h6)'),
|
||||
)
|
||||
.filter(el => el.id && el.hasChildNodes())
|
||||
.map((el) => {
|
||||
const level = Number(el.tagName[1])
|
||||
return {
|
||||
element: el as HTMLHeadElement,
|
||||
title: serializeHeader(el),
|
||||
link: `#${el.id}`,
|
||||
level,
|
||||
}
|
||||
})
|
||||
return resolveHeaders(headers, range)
|
||||
}
|
||||
|
||||
function serializeHeader(h: Element): string {
|
||||
// <hx><a href="#"><span>title</span></a></hx>
|
||||
const anchor = h.firstChild
|
||||
const el = anchor?.firstChild
|
||||
let ret = ''
|
||||
for (const node of Array.from(el?.childNodes ?? [])) {
|
||||
if (node.nodeType === 1) {
|
||||
if (
|
||||
(node as Element).classList.contains('badge-view')
|
||||
|| (node as Element).classList.contains('ignore-header')
|
||||
)
|
||||
continue
|
||||
|
||||
ret += node.textContent
|
||||
}
|
||||
else if (node.nodeType === 3) {
|
||||
ret += node.textContent
|
||||
}
|
||||
}
|
||||
// maybe `<hx><a href="#"></a><a href="xxx"></a</hx>` or more
|
||||
let next = anchor?.nextSibling
|
||||
while (next) {
|
||||
if (next.nodeType === 1 || next.nodeType === 3)
|
||||
ret += next.textContent
|
||||
|
||||
next = next.nextSibling
|
||||
}
|
||||
return ret.trim()
|
||||
}
|
||||
|
||||
export function resolveHeaders(headers: MenuItem[], range?: PlumeThemeLocaleData['outline']): MenuItem[] {
|
||||
if (range === false)
|
||||
return []
|
||||
|
||||
const levelsRange = range || 2
|
||||
|
||||
const [high, low]: [number, number]
|
||||
= typeof levelsRange === 'number'
|
||||
? [levelsRange, levelsRange]
|
||||
: levelsRange === 'deep'
|
||||
? [2, 6]
|
||||
: levelsRange
|
||||
|
||||
headers = headers.filter(h => h.level >= high && h.level <= low)
|
||||
// clear previous caches
|
||||
resolvedHeaders.length = 0
|
||||
// update global header list for active link rendering
|
||||
for (const { element, link } of headers)
|
||||
resolvedHeaders.push({ element, link })
|
||||
|
||||
const ret: MenuItem[] = []
|
||||
// eslint-disable-next-line no-labels, no-restricted-syntax
|
||||
outer: for (let i = 0; i < headers.length; i++) {
|
||||
const cur = headers[i]
|
||||
if (i === 0) {
|
||||
ret.push(cur)
|
||||
}
|
||||
else {
|
||||
for (let j = i - 1; j >= 0; j--) {
|
||||
const prev = headers[j]
|
||||
if (prev.level < cur.level) {
|
||||
;(prev.children || (prev.children = [])).push(cur)
|
||||
// eslint-disable-next-line no-labels
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
ret.push(cur)
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
export function useActiveAnchor(container: Ref<HTMLElement>, marker: Ref<HTMLElement>): void {
|
||||
const { isAsideEnabled } = useAside()
|
||||
|
||||
let prevActiveLink: HTMLAnchorElement | null = null
|
||||
|
||||
const setActiveLink = (): void => {
|
||||
if (!isAsideEnabled.value)
|
||||
return
|
||||
|
||||
const scrollY = window.scrollY
|
||||
const innerHeight = window.innerHeight
|
||||
const offsetHeight = document.body.offsetHeight
|
||||
const isBottom = Math.abs(scrollY + innerHeight - offsetHeight) < 1
|
||||
|
||||
// resolvedHeaders may be repositioned, hidden or fix positioned
|
||||
const headers = resolvedHeaders
|
||||
.map(({ element, link }) => ({
|
||||
link,
|
||||
top: getAbsoluteTop(element),
|
||||
}))
|
||||
.filter(({ top }) => !Number.isNaN(top))
|
||||
.sort((a, b) => a.top - b.top)
|
||||
|
||||
// no headers available for active link
|
||||
if (!headers.length) {
|
||||
activateLink(null)
|
||||
return
|
||||
}
|
||||
|
||||
// page top
|
||||
if (scrollY < 1) {
|
||||
activateLink(null)
|
||||
return
|
||||
}
|
||||
|
||||
// page bottom - highlight last link
|
||||
if (isBottom) {
|
||||
activateLink(headers[headers.length - 1].link)
|
||||
return
|
||||
}
|
||||
|
||||
// find the last header above the top of viewport
|
||||
let activeLink: string | null = null
|
||||
for (const { link, top } of headers) {
|
||||
if (top > scrollY + 144)
|
||||
break
|
||||
|
||||
activeLink = link
|
||||
}
|
||||
activateLink(activeLink)
|
||||
}
|
||||
|
||||
function activateLink(hash: string | null): void {
|
||||
if (prevActiveLink)
|
||||
prevActiveLink.classList.remove('active')
|
||||
|
||||
if (hash == null) {
|
||||
prevActiveLink = null
|
||||
}
|
||||
else {
|
||||
prevActiveLink = container.value.querySelector(
|
||||
`a[href="${decodeURIComponent(hash)}"]`,
|
||||
)
|
||||
}
|
||||
|
||||
const activeLink = prevActiveLink
|
||||
|
||||
if (activeLink) {
|
||||
activeLink.classList.add('active')
|
||||
marker.value.style.top = `${activeLink.offsetTop + 39}px`
|
||||
marker.value.style.opacity = '1'
|
||||
}
|
||||
else {
|
||||
marker.value.style.top = '33px'
|
||||
marker.value.style.opacity = '0'
|
||||
}
|
||||
}
|
||||
|
||||
const onScroll = throttleAndDebounce(setActiveLink, 100)
|
||||
|
||||
onMounted(() => {
|
||||
requestAnimationFrame(setActiveLink)
|
||||
window.addEventListener('scroll', onScroll)
|
||||
})
|
||||
|
||||
onUpdated(() => {
|
||||
// sidebar update means a route change
|
||||
activateLink(location.hash)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('scroll', onScroll)
|
||||
})
|
||||
}
|
||||
|
||||
function getAbsoluteTop(element: HTMLElement): number {
|
||||
let offsetTop = 0
|
||||
while (element !== document.body) {
|
||||
if (element === null) {
|
||||
// child element is:
|
||||
// - not attached to the DOM (display: none)
|
||||
// - set to fixed position (not scrollable)
|
||||
// - body or html element (null offsetParent)
|
||||
return Number.NaN
|
||||
}
|
||||
offsetTop += element.offsetTop
|
||||
element = element.offsetParent as HTMLElement
|
||||
}
|
||||
return offsetTop
|
||||
}
|
||||
@ -11,6 +11,7 @@ const FALLBACK_OPTIONS: PlumeThemeLocaleData = {
|
||||
article: '/article/',
|
||||
notes: { link: '/', dir: '/notes/', notes: [] },
|
||||
navbarSocialInclude: ['github', 'twitter', 'discord', 'facebook'],
|
||||
outline: [2, 3],
|
||||
|
||||
// page meta
|
||||
editLink: true,
|
||||
|
||||
@ -119,6 +119,7 @@ export interface PlumeThemePageFrontmatter {
|
||||
contributors?: boolean
|
||||
prev?: string | NavItemWithLink
|
||||
next?: string | NavItemWithLink
|
||||
outline?: false | number | [number, number] | 'deep'
|
||||
backToTop?: boolean
|
||||
externalLink?: boolean
|
||||
readingTime?: boolean
|
||||
|
||||
@ -74,6 +74,8 @@ export interface PlumeThemeLocaleData extends LocaleData {
|
||||
*/
|
||||
notes?: false | NotesDataOptions
|
||||
|
||||
outline?: false | number | [number, number] | 'deep'
|
||||
|
||||
/**
|
||||
* language text
|
||||
*/
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user