Merge branch 'main' into refactor/local-search

pull/3374/head
Yuxuan Zhang 2 years ago committed by GitHub
commit 198228fcea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,3 +1,21 @@
# [1.0.0-rc.34](https://github.com/vuejs/vitepress/compare/v1.0.0-rc.33...v1.0.0-rc.34) (2023-12-30)
### Bug Fixes
- **build:** clear cache after build ([9568fea](https://github.com/vuejs/vitepress/commit/9568fea8fc50e625c8ef27c588eca3dbe5a44e81)), closes [#3363](https://github.com/vuejs/vitepress/issues/3363)
- **default-theme:** remove use of reactify for search i18n ([8687b86](https://github.com/vuejs/vitepress/commit/8687b86e1e00ae39ff9c8173aef04eb8a9cda0a8))
- print errors when importing an invalid dynamic route ([#3201](https://github.com/vuejs/vitepress/issues/3201)) ([6d89a08](https://github.com/vuejs/vitepress/commit/6d89a08cb76674f4d92f54218f8af5624bcf4c47))
- remove double title from home pages ([9f1f04e](https://github.com/vuejs/vitepress/commit/9f1f04e00a9722ec7369941c40d3d8ad86f61d35)), closes [#3375](https://github.com/vuejs/vitepress/issues/3375)
- **theme/i18n:** support customizing dark mode switch titles ([#3311](https://github.com/vuejs/vitepress/issues/3311)) ([50c9758](https://github.com/vuejs/vitepress/commit/50c9758d3fa1b60aad5399a0db890644ac44a522))
### Features
- support custom image lazy loading ([#3346](https://github.com/vuejs/vitepress/issues/3346)) ([55be3f1](https://github.com/vuejs/vitepress/commit/55be3f14d79eb578c9aa2e3bc7a90205c910005d))
- support dir in frontmatter ([#3353](https://github.com/vuejs/vitepress/issues/3353)) ([203446d](https://github.com/vuejs/vitepress/commit/203446d69193483a46e1082bba5fbad0e35966fb))
- **theme/i18n:** allow customizing sponsor link's text ([#3276](https://github.com/vuejs/vitepress/issues/3276)) ([9c20e3b](https://github.com/vuejs/vitepress/commit/9c20e3b5f80e4197c14c20fa751ec3c8c8219e8e))
- **theme:** allow using VPBadge classes in sidebar ([#3391](https://github.com/vuejs/vitepress/issues/3391)) ([50a774e](https://github.com/vuejs/vitepress/commit/50a774ea7c70bb200e12c176d6691ab7144a73f9))
- **theme:** new design for local nav and global header ([#3359](https://github.com/vuejs/vitepress/issues/3359)) ([d10bf42](https://github.com/vuejs/vitepress/commit/d10bf42c2632f1aacb905bc01b36274e9004cbd9))
# [1.0.0-rc.33](https://github.com/vuejs/vitepress/compare/v1.0.0-rc.32...v1.0.0-rc.33) (2023-12-26) # [1.0.0-rc.33](https://github.com/vuejs/vitepress/compare/v1.0.0-rc.32...v1.0.0-rc.33) (2023-12-26)
### Features ### Features

@ -86,6 +86,11 @@ const sidebar: DefaultTheme.Config['sidebar'] = {
export default defineConfig({ export default defineConfig({
title: 'Example', title: 'Example',
description: 'An example app using VitePress.', description: 'An example app using VitePress.',
markdown: {
image: {
lazyLoading: true
}
},
themeConfig: { themeConfig: {
sidebar, sidebar,
search: { search: {

@ -196,3 +196,7 @@ export default config
## Markdown File Inclusion with Range without End ## Markdown File Inclusion with Range without End
<!--@include: ./foo.md{6,}--> <!--@include: ./foo.md{6,}-->
## Image Lazy Loading
![vitepress logo](/vitepress.png)

@ -65,7 +65,7 @@ describe('Table of Contents', () => {
test('render toc', async () => { test('render toc', async () => {
const items = page.locator('#table-of-contents + nav ul li') const items = page.locator('#table-of-contents + nav ul li')
const count = await items.count() const count = await items.count()
expect(count).toBe(35) expect(count).toBe(36)
}) })
}) })
@ -280,3 +280,10 @@ describe('Markdown File Inclusion', () => {
expect(await p.textContent()).not.toContain('title') expect(await p.textContent()).not.toContain('title')
}) })
}) })
describe('Image Lazy Loading', () => {
test('render loading="lazy" in the <img> tag', async () => {
const img = page.locator('#image-lazy-loading + p img')
expect(await img.getAttribute('loading')).toBe('lazy')
})
})

@ -847,6 +847,21 @@ $$ x = {-b \pm \sqrt{b^2-4ac} \over 2a} $$
| $\nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} = \vec{\mathbf{0}}$ | curl of $\vec{\mathbf{E}}$ is proportional to the rate of change of $\vec{\mathbf{B}}$ | | $\nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} = \vec{\mathbf{0}}$ | curl of $\vec{\mathbf{E}}$ is proportional to the rate of change of $\vec{\mathbf{B}}$ |
| $\nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} = \frac{4\pi}{c}\vec{\mathbf{j}} \nabla \cdot \vec{\mathbf{E}} = 4 \pi \rho$ | _wha?_ | | $\nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} = \frac{4\pi}{c}\vec{\mathbf{j}} \nabla \cdot \vec{\mathbf{E}} = 4 \pi \rho$ | _wha?_ |
## Image Lazy Loading
You can enable lazy loading for each image added via markdown by setting `lazyLoading` to `true` in your config file:
```js
export default {
markdown: {
image: {
// image lazy loading is disabled by default
lazyLoading: true
}
}
}
```
## Advanced Configuration ## Advanced Configuration
VitePress uses [markdown-it](https://github.com/markdown-it/markdown-it) as the Markdown renderer. A lot of the extensions above are implemented via custom plugins. You can further customize the `markdown-it` instance using the `markdown` option in `.vitepress/config.js`: VitePress uses [markdown-it](https://github.com/markdown-it/markdown-it) as the Markdown renderer. A lot of the extensions above are implemented via custom plugins. You can further customize the `markdown-it` instance using the `markdown` option in `.vitepress/config.js`:

@ -406,6 +406,20 @@ export interface DocFooter {
Can be used to customize the dark mode switch label. This label is only displayed in the mobile view. Can be used to customize the dark mode switch label. This label is only displayed in the mobile view.
## lightModeSwitchTitle
- Type: `string`
- Default: `Switch to light theme`
Can be used to customize the light mode switch title that appears on hovering.
## darkModeSwitchTitle
- Type: `string`
- Default: `Switch to dark theme`
Can be used to customize the dark mode switch title that appears on hovering.
## sidebarMenuLabel ## sidebarMenuLabel
- Type: `string` - Type: `string`

@ -24,6 +24,62 @@ export default {
} }
``` ```
:::details Dynamic (Async) Config
If you need to dynamically generate the config, you can also default export a function. For example:
```ts
import { defineConfig } from 'vitepress'
export default async () => defineConfig({
const posts = await (await fetch('https://my-cms.com/blog-posts')).json()
return {
// app level config options
lang: 'en-US',
title: 'VitePress',
description: 'Vite & Vue powered static site generator.',
// theme level config options
themeConfig: {
sidebar: [
...posts.map((post) => ({
text: post.name,
link: `/posts/${post.name}`
}))
]
}
}
})
```
You can also use top-level `await`. For example:
```ts
import { defineConfig } from 'vitepress'
const posts = await (await fetch('https://my-cms.com/blog-posts')).json()
export default defineConfig({
// app level config options
lang: 'en-US',
title: 'VitePress',
description: 'Vite & Vue powered static site generator.',
// theme level config options
themeConfig: {
sidebar: [
...posts.map((post) => ({
text: post.name,
link: `/posts/${post.name}`
}))
]
}
})
```
:::
### Config Intellisense ### Config Intellisense
Using the `defineConfig` helper will provide TypeScript-powered intellisense for config options. Assuming your IDE supports it, this should work in both JavaScript and TypeScript. Using the `defineConfig` helper will provide TypeScript-powered intellisense for config options. Assuming your IDE supports it, this should work in both JavaScript and TypeScript.

@ -1,9 +1,9 @@
{ {
"name": "vitepress", "name": "vitepress",
"version": "1.0.0-rc.33", "version": "1.0.0-rc.34",
"description": "Vite & Vue powered static site generator", "description": "Vite & Vue powered static site generator",
"type": "module", "type": "module",
"packageManager": "pnpm@8.12.1", "packageManager": "pnpm@8.13.1",
"main": "dist/node/index.js", "main": "dist/node/index.js",
"types": "types/index.d.ts", "types": "types/index.d.ts",
"exports": { "exports": {
@ -93,18 +93,19 @@
"@docsearch/css": "^3.5.2", "@docsearch/css": "^3.5.2",
"@docsearch/js": "^3.5.2", "@docsearch/js": "^3.5.2",
"@types/markdown-it": "^13.0.7", "@types/markdown-it": "^13.0.7",
"@vitejs/plugin-vue": "^5.0.0", "@vitejs/plugin-vue": "^5.0.2",
"@vue/devtools-api": "^6.5.1", "@vue/devtools-api": "^6.5.1",
"@vueuse/core": "^10.7.0", "@vueuse/core": "^10.7.1",
"@vueuse/integrations": "^10.7.0", "@vueuse/integrations": "^10.7.1",
"focus-trap": "^7.5.4", "focus-trap": "^7.5.4",
"mark.js": "8.11.1", "mark.js": "8.11.1",
"minisearch": "^6.3.0", "minisearch": "^6.3.0",
"mrmime": "^2.0.0", "mrmime": "^2.0.0",
"shikiji": "^0.9.12", "shikiji": "^0.9.15",
"shikiji-transformers": "^0.9.12", "shikiji-core": "^0.9.15",
"shikiji-transformers": "^0.9.15",
"vite": "^5.0.10", "vite": "^5.0.10",
"vue": "^3.4.0-rc.2" "vue": "^3.4.3"
}, },
"peerDependencies": { "peerDependencies": {
"markdown-it-mathjax3": "^4.3.2", "markdown-it-mathjax3": "^4.3.2",
@ -144,16 +145,16 @@
"@types/markdown-it-emoji": "^2.0.4", "@types/markdown-it-emoji": "^2.0.4",
"@types/micromatch": "^4.0.6", "@types/micromatch": "^4.0.6",
"@types/minimist": "^1.2.5", "@types/minimist": "^1.2.5",
"@types/node": "^20.10.5", "@types/node": "^20.10.6",
"@types/postcss-prefix-selector": "^1.16.3", "@types/postcss-prefix-selector": "^1.16.3",
"@types/prompts": "^2.4.9", "@types/prompts": "^2.4.9",
"@vue/shared": "^3.3.13", "@vue/shared": "^3.4.3",
"chokidar": "^3.5.3", "chokidar": "^3.5.3",
"compression": "^1.7.4", "compression": "^1.7.4",
"conventional-changelog-cli": "^4.1.0", "conventional-changelog-cli": "^4.1.0",
"cross-spawn": "^7.0.3", "cross-spawn": "^7.0.3",
"debug": "^4.3.4", "debug": "^4.3.4",
"esbuild": "^0.19.10", "esbuild": "^0.19.11",
"escape-html": "^1.0.3", "escape-html": "^1.0.3",
"execa": "^8.0.1", "execa": "^8.0.1",
"fast-glob": "^3.3.2", "fast-glob": "^3.3.2",
@ -174,7 +175,7 @@
"nanoid": "^5.0.4", "nanoid": "^5.0.4",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"ora": "^8.0.1", "ora": "^8.0.1",
"p-map": "^7.0.0", "p-map": "^7.0.1",
"path-to-regexp": "^6.2.1", "path-to-regexp": "^6.2.1",
"picocolors": "^1.0.0", "picocolors": "^1.0.0",
"pkg-dir": "^8.0.0", "pkg-dir": "^8.0.0",
@ -185,18 +186,17 @@
"prompts": "^2.4.2", "prompts": "^2.4.2",
"punycode": "^2.3.1", "punycode": "^2.3.1",
"rimraf": "^5.0.5", "rimraf": "^5.0.5",
"rollup": "^4.9.1", "rollup": "^4.9.2",
"rollup-plugin-dts": "^6.1.0", "rollup-plugin-dts": "^6.1.0",
"rollup-plugin-esbuild": "^6.1.0", "rollup-plugin-esbuild": "^6.1.0",
"semver": "^7.5.4", "semver": "^7.5.4",
"shikiji-core": "^0.9.12",
"simple-git-hooks": "^2.9.0", "simple-git-hooks": "^2.9.0",
"sirv": "^2.0.4", "sirv": "^2.0.4",
"sitemap": "^7.1.1", "sitemap": "^7.1.1",
"supports-color": "^9.4.0", "supports-color": "^9.4.0",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"vitest": "^1.1.0", "vitest": "^1.1.0",
"vue-tsc": "^1.8.26", "vue-tsc": "^1.8.27",
"wait-on": "^7.2.0" "wait-on": "^7.2.0"
}, },
"simple-git-hooks": { "simple-git-hooks": {

File diff suppressed because it is too large Load Diff

@ -89,7 +89,7 @@ export function initData(route: Route): VitePressData {
frontmatter: computed(() => route.data.frontmatter), frontmatter: computed(() => route.data.frontmatter),
params: computed(() => route.data.params), params: computed(() => route.data.params),
lang: computed(() => site.value.lang), lang: computed(() => site.value.lang),
dir: computed(() => site.value.dir), dir: computed(() => route.data.frontmatter.dir || site.value.dir || 'ltr'),
localeIndex: computed(() => site.value.localeIndex || 'root'), localeIndex: computed(() => site.value.localeIndex || 'root'),
title: computed(() => { title: computed(() => {
return createTitle(site.value, route.data) return createTitle(site.value, route.data)

@ -14,7 +14,7 @@ withDefaults(defineProps<Props>(), {
</span> </span>
</template> </template>
<style scoped> <style>
.VPBadge { .VPBadge {
display: inline-block; display: inline-block;
margin-left: 2px; margin-left: 2px;
@ -27,6 +27,16 @@ withDefaults(defineProps<Props>(), {
transform: translateY(-2px); transform: translateY(-2px);
} }
.VPBadge.small {
padding: 0 6px;
line-height: 18px;
font-size: 11px;
}
.VPDocFooter .VPBadge {
display: none;
}
.vp-doc h1 > .VPBadge { .vp-doc h1 > .VPBadge {
margin-top: 4px; margin-top: 4px;
vertical-align: top; vertical-align: top;

@ -5,7 +5,6 @@ import { useData } from '../composables/data'
import { useSidebar } from '../composables/sidebar' import { useSidebar } from '../composables/sidebar'
import VPDocAside from './VPDocAside.vue' import VPDocAside from './VPDocAside.vue'
import VPDocFooter from './VPDocFooter.vue' import VPDocFooter from './VPDocFooter.vue'
import VPDocOutlineDropdown from './VPDocOutlineDropdown.vue'
const { theme } = useData() const { theme } = useData()
@ -43,7 +42,6 @@ const pageName = computed(() =>
<div class="content"> <div class="content">
<div class="content-container"> <div class="content-container">
<slot name="doc-before" /> <slot name="doc-before" />
<VPDocOutlineDropdown />
<main class="main"> <main class="main">
<Content <Content
class="vp-doc" class="vp-doc"
@ -70,16 +68,6 @@ const pageName = computed(() =>
width: 100%; width: 100%;
} }
.VPDoc .VPDocOutlineDropdown {
display: none;
}
@media (min-width: 960px) and (max-width: 1279px) {
.VPDoc .VPDocOutlineDropdown {
display: block;
}
}
@media (min-width: 768px) { @media (min-width: 768px) {
.VPDoc { .VPDoc {
padding: 48px 32px 128px; padding: 48px 32px 128px;
@ -88,7 +76,7 @@ const pageName = computed(() =>
@media (min-width: 960px) { @media (min-width: 960px) {
.VPDoc { .VPDoc {
padding: 32px 32px 0; padding: 48px 32px 0;
} }
.VPDoc:not(.has-sidebar) .container { .VPDoc:not(.has-sidebar) .container {
@ -147,7 +135,7 @@ const pageName = computed(() =>
.aside-container { .aside-container {
position: fixed; position: fixed;
top: 0; top: 0;
padding-top: calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + var(--vp-doc-top-height, 0px) + 32px); padding-top: calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + var(--vp-doc-top-height, 0px) + 48px);
width: 224px; width: 224px;
height: 100vh; height: 100vh;
overflow-x: hidden; overflow-x: hidden;
@ -171,7 +159,7 @@ const pageName = computed(() =>
.aside-content { .aside-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: calc(100vh - (var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 32px)); min-height: calc(100vh - (var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 48px));
padding-bottom: 32px; padding-bottom: 32px;
} }

@ -80,9 +80,8 @@ useActiveAnchor(container, marker)
} }
.outline-title { .outline-title {
letter-spacing: 0.4px; line-height: 32px;
line-height: 28px; font-size: 14px;
font-size: 13px;
font-weight: 600; font-weight: 600;
} }
</style> </style>

@ -1,85 +0,0 @@
<script setup lang="ts">
import { ref, shallowRef } from 'vue'
import { useData } from '../composables/data'
import { getHeaders, resolveTitle, type MenuItem } from '../composables/outline'
import VPDocOutlineItem from './VPDocOutlineItem.vue'
import { onContentUpdated } from 'vitepress'
import VPIconChevronRight from './icons/VPIconChevronRight.vue'
const { frontmatter, theme } = useData()
const open = ref(false)
onContentUpdated(() => {
open.value = false
})
const headers = shallowRef<MenuItem[]>([])
onContentUpdated(() => {
headers.value = getHeaders(
frontmatter.value.outline ?? theme.value.outline
)
})
</script>
<template>
<div class="VPDocOutlineDropdown" v-if="headers.length > 0">
<button @click="open = !open" :class="{ open }">
{{ resolveTitle(theme) }}
<VPIconChevronRight class="icon" />
</button>
<div class="items" v-if="open">
<VPDocOutlineItem :headers="headers" />
</div>
</div>
</template>
<style scoped>
.VPDocOutlineDropdown {
margin-bottom: 48px;
}
.VPDocOutlineDropdown button {
display: block;
font-size: 14px;
font-weight: 500;
line-height: 24px;
border: 1px solid var(--vp-c-border);
padding: 4px 12px;
color: var(--vp-c-text-2);
background-color: var(--vp-c-default-soft);
border-radius: 8px;
transition: color 0.5s;
}
.VPDocOutlineDropdown button:hover {
color: var(--vp-c-text-1);
transition: color 0.25s;
}
.VPDocOutlineDropdown button.open {
color: var(--vp-c-text-1);
}
.icon {
display: inline-block;
vertical-align: middle;
width: 16px;
height: 16px;
fill: currentColor;
}
:deep(.outline-link) {
font-size: 14px;
font-weight: 400;
}
.open > .icon {
transform: rotate(90deg);
}
.items {
margin-top: 12px;
border-left: 1px solid var(--vp-c-divider);
}
</style>

@ -14,7 +14,7 @@ function onClick({ target: el }: Event) {
</script> </script>
<template> <template>
<ul :class="root ? 'root' : 'nested'"> <ul class="VPDocOutlineItem" :class="root ? 'root' : 'nested'">
<li v-for="{ children, link, title } in headers"> <li v-for="{ children, link, title } in headers">
<a class="outline-link" :href="link" @click="onClick" :title="title">{{ title }}</a> <a class="outline-link" :href="link" @click="onClick" :title="title">{{ title }}</a>
<template v-if="children?.length"> <template v-if="children?.length">
@ -31,18 +31,20 @@ function onClick({ target: el }: Event) {
} }
.nested { .nested {
padding-right: 16px;
padding-left: 16px; padding-left: 16px;
} }
.outline-link { .outline-link {
display: block; display: block;
line-height: 28px; line-height: 32px;
font-size: 14px;
font-weight: 400;
color: var(--vp-c-text-2); color: var(--vp-c-text-2);
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
transition: color 0.5s; transition: color 0.5s;
font-weight: 400;
} }
.outline-link:hover, .outline-link:hover,

@ -1,9 +1,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useWindowScroll } from '@vueuse/core' import { useWindowScroll } from '@vueuse/core'
import { onContentUpdated } from 'vitepress' import { onContentUpdated } from 'vitepress'
import { computed, onMounted, ref, shallowRef } from 'vue' import { computed, onMounted, ref } from 'vue'
import { useData } from '../composables/data' import { useData } from '../composables/data'
import { getHeaders, type MenuItem } from '../composables/outline' import { useLocalNav } from '../composables/local-nav'
import { getHeaders } from '../composables/outline'
import { useSidebar } from '../composables/sidebar' import { useSidebar } from '../composables/sidebar'
import VPLocalNavOutlineDropdown from './VPLocalNavOutlineDropdown.vue' import VPLocalNavOutlineDropdown from './VPLocalNavOutlineDropdown.vue'
import VPIconAlignLeft from './icons/VPIconAlignLeft.vue' import VPIconAlignLeft from './icons/VPIconAlignLeft.vue'
@ -18,9 +19,9 @@ defineEmits<{
const { theme, frontmatter } = useData() const { theme, frontmatter } = useData()
const { hasSidebar } = useSidebar() const { hasSidebar } = useSidebar()
const { headers } = useLocalNav()
const { y } = useWindowScroll() const { y } = useWindowScroll()
const headers = shallowRef<MenuItem[]>([])
const navHeight = ref(0) const navHeight = ref(0)
onMounted(() => { onMounted(() => {
@ -36,37 +37,44 @@ onContentUpdated(() => {
}) })
const empty = computed(() => { const empty = computed(() => {
return headers.value.length === 0 && !hasSidebar.value return headers.value.length === 0
})
const emptyAndNoSidebar = computed(() => {
return empty.value && !hasSidebar.value
}) })
const classes = computed(() => { const classes = computed(() => {
return { return {
VPLocalNav: true, VPLocalNav: true,
fixed: empty.value, 'has-sidebar': hasSidebar.value,
'reached-top': y.value >= navHeight.value empty: empty.value,
fixed: emptyAndNoSidebar.value
} }
}) })
</script> </script>
<template> <template>
<div <div
v-if="frontmatter.layout !== 'home' && (!empty || y >= navHeight)" v-if="frontmatter.layout !== 'home' && (!emptyAndNoSidebar || y >= navHeight)"
:class="classes" :class="classes"
> >
<button <div class="container">
v-if="hasSidebar" <button
class="menu" v-if="hasSidebar"
:aria-expanded="open" class="menu"
aria-controls="VPSidebarNav" :aria-expanded="open"
@click="$emit('open-menu')" aria-controls="VPSidebarNav"
> @click="$emit('open-menu')"
<VPIconAlignLeft class="menu-icon" /> >
<span class="menu-text"> <VPIconAlignLeft class="menu-icon" />
{{ theme.sidebarMenuLabel || 'Menu' }} <span class="menu-text">
</span> {{ theme.sidebarMenuLabel || 'Menu' }}
</button> </span>
</button>
<VPLocalNavOutlineDropdown :headers="headers" :navHeight="navHeight" />
<VPLocalNavOutlineDropdown :headers="headers" :navHeight="navHeight" />
</div>
</div> </div>
</template> </template>
@ -77,10 +85,6 @@ const classes = computed(() => {
/*rtl:ignore*/ /*rtl:ignore*/
left: 0; left: 0;
z-index: var(--vp-z-index-local-nav); z-index: var(--vp-z-index-local-nav);
display: flex;
justify-content: space-between;
align-items: center;
border-top: 1px solid var(--vp-c-gutter);
border-bottom: 1px solid var(--vp-c-gutter); border-bottom: 1px solid var(--vp-c-gutter);
padding-top: var(--vp-layout-top-height, 0px); padding-top: var(--vp-layout-top-height, 0px);
width: 100%; width: 100%;
@ -91,16 +95,38 @@ const classes = computed(() => {
position: fixed; position: fixed;
} }
.VPLocalNav.reached-top { @media (min-width: 960px) {
border-top-color: transparent; .VPLocalNav {
top: var(--vp-nav-height);
}
.VPLocalNav.has-sidebar {
padding-left: var(--vp-sidebar-width);
}
.VPLocalNav.empty {
display: none;
}
} }
@media (min-width: 960px) { @media (min-width: 1280px) {
.VPLocalNav { .VPLocalNav {
display: none; display: none;
} }
} }
@media (min-width: 1440px) {
.VPLocalNav.has-sidebar {
padding-left: calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width));
}
}
.container {
display: flex;
justify-content: space-between;
align-items: center;
}
.menu { .menu {
display: flex; display: flex;
align-items: center; align-items: center;
@ -123,6 +149,12 @@ const classes = computed(() => {
} }
} }
@media (min-width: 960px) {
.menu {
display: none;
}
}
.menu-icon { .menu-icon {
margin-right: 8px; margin-right: 8px;
width: 16px; width: 16px;

@ -1,4 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { onClickOutside, onKeyStroke } from '@vueuse/core'
import { onContentUpdated } from 'vitepress' import { onContentUpdated } from 'vitepress'
import { nextTick, ref } from 'vue' import { nextTick, ref } from 'vue'
import { useData } from '../composables/data' import { useData } from '../composables/data'
@ -14,8 +15,17 @@ const props = defineProps<{
const { theme } = useData() const { theme } = useData()
const open = ref(false) const open = ref(false)
const vh = ref(0) const vh = ref(0)
const main = ref<HTMLDivElement>()
const items = ref<HTMLDivElement>() const items = ref<HTMLDivElement>()
onClickOutside(main, () => {
open.value = false
})
onKeyStroke('Escape', () => {
open.value = false
})
onContentUpdated(() => { onContentUpdated(() => {
open.value = false open.value = false
}) })
@ -44,7 +54,11 @@ function scrollToTop() {
</script> </script>
<template> <template>
<div class="VPLocalNavOutlineDropdown" :style="{ '--vp-vh': vh + 'px' }"> <div
class="VPLocalNavOutlineDropdown"
:style="{ '--vp-vh': vh + 'px' }"
ref="main"
>
<button @click="toggle" :class="{ open }" v-if="headers.length > 0"> <button @click="toggle" :class="{ open }" v-if="headers.length > 0">
{{ resolveTitle(theme) }} {{ resolveTitle(theme) }}
<VPIconChevronRight class="icon" /> <VPIconChevronRight class="icon" />
@ -53,11 +67,7 @@ function scrollToTop() {
{{ theme.returnToTopLabel || 'Return to top' }} {{ theme.returnToTopLabel || 'Return to top' }}
</button> </button>
<Transition name="flyout"> <Transition name="flyout">
<div v-if="open" <div v-if="open" ref="items" class="items" @click="onItemClick">
ref="items"
class="items"
@click="onItemClick"
>
<div class="header"> <div class="header">
<a class="top-link" href="#" @click="scrollToTop"> <a class="top-link" href="#" @click="scrollToTop">
{{ theme.returnToTopLabel || 'Return to top' }} {{ theme.returnToTopLabel || 'Return to top' }}
@ -76,6 +86,12 @@ function scrollToTop() {
padding: 12px 20px 11px; padding: 12px 20px 11px;
} }
@media (min-width: 960px) {
.VPLocalNavOutlineDropdown {
padding: 12px 36px 11px;
}
}
.VPLocalNavOutlineDropdown button { .VPLocalNavOutlineDropdown button {
display: block; display: block;
font-size: 12px; font-size: 12px;
@ -95,6 +111,12 @@ function scrollToTop() {
color: var(--vp-c-text-1); color: var(--vp-c-text-1);
} }
@media (min-width: 960px) {
.VPLocalNavOutlineDropdown button {
font-size: 14px;
}
}
.icon { .icon {
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
@ -104,18 +126,13 @@ function scrollToTop() {
fill: currentColor; fill: currentColor;
} }
:deep(.outline-link) {
font-size: 14px;
padding: 2px 0;
}
.open > .icon { .open > .icon {
transform: rotate(90deg); transform: rotate(90deg);
} }
.items { .items {
position: absolute; position: absolute;
top: 64px; top: 40px;
right: 16px; right: 16px;
left: 16px; left: 16px;
display: grid; display: grid;
@ -128,6 +145,14 @@ function scrollToTop() {
box-shadow: var(--vp-shadow-3); box-shadow: var(--vp-shadow-3);
} }
@media (min-width: 960px) {
.items {
right: auto;
left: calc(var(--vp-sidebar-width) + 32px);
width: 320px;
}
}
.header { .header {
background-color: var(--vp-c-bg-soft); background-color: var(--vp-c-bg-soft);
} }
@ -147,11 +172,11 @@ function scrollToTop() {
} }
.flyout-enter-active { .flyout-enter-active {
transition: all .2s ease-out; transition: all 0.2s ease-out;
} }
.flyout-leave-active { .flyout-leave-active {
transition: all .15s ease-in; transition: all 0.15s ease-in;
} }
.flyout-enter-from, .flyout-enter-from,

@ -2,6 +2,7 @@
import { useWindowScroll } from '@vueuse/core' import { useWindowScroll } from '@vueuse/core'
import { ref, watchPostEffect } from 'vue' import { ref, watchPostEffect } from 'vue'
import { useData } from '../composables/data' import { useData } from '../composables/data'
import { useLocalNav } from '../composables/local-nav'
import { useSidebar } from '../composables/sidebar' import { useSidebar } from '../composables/sidebar'
import VPNavBarAppearance from './VPNavBarAppearance.vue' import VPNavBarAppearance from './VPNavBarAppearance.vue'
import VPNavBarExtra from './VPNavBarExtra.vue' import VPNavBarExtra from './VPNavBarExtra.vue'
@ -22,6 +23,7 @@ defineEmits<{
const { y } = useWindowScroll() const { y } = useWindowScroll()
const { hasSidebar } = useSidebar() const { hasSidebar } = useSidebar()
const { hasLocalNav } = useLocalNav()
const { frontmatter } = useData() const { frontmatter } = useData()
const classes = ref<Record<string, boolean>>({}) const classes = ref<Record<string, boolean>>({})
@ -29,6 +31,7 @@ const classes = ref<Record<string, boolean>>({})
watchPostEffect(() => { watchPostEffect(() => {
classes.value = { classes.value = {
'has-sidebar': hasSidebar.value, 'has-sidebar': hasSidebar.value,
'has-local-nav': hasLocalNav.value,
top: frontmatter.value.layout === 'home' && y.value === 0, top: frontmatter.value.layout === 'home' && y.value === 0,
} }
}) })
@ -36,59 +39,76 @@ watchPostEffect(() => {
<template> <template>
<div class="VPNavBar" :class="classes"> <div class="VPNavBar" :class="classes">
<div class="container"> <div class="wrapper">
<div class="title"> <div class="container">
<VPNavBarTitle> <div class="title">
<template #nav-bar-title-before><slot name="nav-bar-title-before" /></template> <VPNavBarTitle>
<template #nav-bar-title-after><slot name="nav-bar-title-after" /></template> <template #nav-bar-title-before><slot name="nav-bar-title-before" /></template>
</VPNavBarTitle> <template #nav-bar-title-after><slot name="nav-bar-title-after" /></template>
</div> </VPNavBarTitle>
</div>
<div class="content"> <div class="content">
<div class="curtain" /> <div class="content-body">
<div class="content-body"> <slot name="nav-bar-content-before" />
<slot name="nav-bar-content-before" /> <VPNavBarSearch class="search" />
<VPNavBarSearch class="search" /> <VPNavBarMenu class="menu" />
<VPNavBarMenu class="menu" /> <VPNavBarTranslations class="translations" />
<VPNavBarTranslations class="translations" /> <VPNavBarAppearance class="appearance" />
<VPNavBarAppearance class="appearance" /> <VPNavBarSocialLinks class="social-links" />
<VPNavBarSocialLinks class="social-links" /> <VPNavBarExtra class="extra" />
<VPNavBarExtra class="extra" /> <slot name="nav-bar-content-after" />
<slot name="nav-bar-content-after" /> <VPNavBarHamburger class="hamburger" :active="isScreenOpen" @click="$emit('toggle-screen')" />
<VPNavBarHamburger class="hamburger" :active="isScreenOpen" @click="$emit('toggle-screen')" /> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="divider">
<div class="divider-line" />
</div>
</div> </div>
</template> </template>
<style scoped> <style scoped>
.VPNavBar { .VPNavBar {
position: relative; position: relative;
border-bottom: 1px solid transparent;
padding: 0 8px 0 24px;
height: var(--vp-nav-height); height: var(--vp-nav-height);
pointer-events: none; pointer-events: none;
white-space: nowrap; white-space: nowrap;
transition: background-color 0.5s;
} }
@media (min-width: 768px) { .VPNavBar.has-local-nav {
.VPNavBar { background-color: var(--vp-nav-bg-color);
padding: 0 32px;
}
} }
@media (min-width: 960px) { @media (min-width: 960px) {
.VPNavBar.has-sidebar { .VPNavBar.has-local-nav {
padding: 0; background-color: transparent;
} }
.VPNavBar:not(.has-sidebar):not(.top) { .VPNavBar:not(.has-sidebar):not(.top) {
border-bottom-color: var(--vp-c-gutter);
background-color: var(--vp-nav-bg-color); background-color: var(--vp-nav-bg-color);
} }
} }
.wrapper {
padding: 0 8px 0 24px;
}
@media (min-width: 768px) {
.wrapper {
padding: 0 32px;
}
}
@media (min-width: 960px) {
.VPNavBar.has-sidebar .wrapper {
padding: 0;
}
}
.container { .container {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -163,15 +183,19 @@ watchPostEffect(() => {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
align-items: center; align-items: center;
height: calc(var(--vp-nav-height) - 1px); height: var(--vp-nav-height);
transition: background-color 0.5s; transition: background-color 0.5s;
} }
@media (min-width: 960px) { @media (min-width: 960px) {
.VPNavBar:not(.top) .content-body{ .VPNavBar:not(.top) .content-body {
position: relative; position: relative;
background-color: var(--vp-nav-bg-color); background-color: var(--vp-nav-bg-color);
} }
.VPNavBar:not(.has-sidebar):not(.top) .content-body {
background-color: transparent;
}
} }
@media (max-width: 767px) { @media (max-width: 767px) {
@ -206,27 +230,40 @@ watchPostEffect(() => {
margin-right: -8px; margin-right: -8px;
} }
.divider {
width: 100%;
height: 1px;
}
@media (min-width: 960px) { @media (min-width: 960px) {
.VPNavBar.has-sidebar .curtain { .VPNavBar.has-sidebar .divider {
position: absolute; padding-left: var(--vp-sidebar-width);
right: 0;
bottom: -31px;
width: calc(100% - var(--vp-sidebar-width));
height: 32px;
} }
}
.VPNavBar.has-sidebar .curtain::before { @media (min-width: 1440px) {
display: block; .VPNavBar.has-sidebar .divider {
width: 100%; padding-left: calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width));
height: 32px;
background: linear-gradient(var(--vp-c-bg), transparent 70%);
content: "";
} }
} }
@media (min-width: 1440px) { .divider-line {
.VPNavBar.has-sidebar .curtain { width: 100%;
width: calc(100% - ((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width))); height: 1px;
transition: background-color 0.5s;
}
.VPNavBar.has-local-nav .divider-line {
background-color: var(--vp-c-gutter);
}
@media (min-width: 960px) {
.VPNavBar:not(.top) .divider-line {
background-color: var(--vp-c-gutter);
}
.VPNavBar:not(.has-sidebar):not(.top) .divider {
background-color: var(--vp-c-gutter);
} }
} }
</style> </style>

@ -87,7 +87,6 @@ watch(
@media (min-width: 960px) { @media (min-width: 960px) {
.VPSidebar { .VPSidebar {
z-index: 1;
padding-top: var(--vp-nav-height); padding-top: var(--vp-nav-height);
width: var(--vp-sidebar-width); width: var(--vp-sidebar-width);
max-width: 100%; max-width: 100%;

@ -5,14 +5,16 @@ import VPSwitch from './VPSwitch.vue'
import VPIconMoon from './icons/VPIconMoon.vue' import VPIconMoon from './icons/VPIconMoon.vue'
import VPIconSun from './icons/VPIconSun.vue' import VPIconSun from './icons/VPIconSun.vue'
const { isDark } = useData() const { isDark, theme } = useData()
const toggleAppearance = inject('toggle-appearance', () => { const toggleAppearance = inject('toggle-appearance', () => {
isDark.value = !isDark.value isDark.value = !isDark.value
}) })
const switchTitle = computed(() => { const switchTitle = computed(() => {
return isDark.value ? 'Switch to light theme' : 'Switch to dark theme' return isDark.value
? theme.value.lightModeSwitchTitle || 'Switch to light theme'
: theme.value.darkModeSwitchTitle || 'Switch to dark theme'
}) })
</script> </script>

@ -0,0 +1,24 @@
import { onContentUpdated } from 'vitepress'
import { type DefaultTheme } from 'vitepress/theme'
import { computed, shallowRef } from 'vue'
import { getHeaders, type MenuItem } from '../composables/outline'
import { useData } from './data'
export function useLocalNav(): DefaultTheme.DocLocalNav {
const { theme, frontmatter } = useData()
const headers = shallowRef<MenuItem[]>([])
const hasLocalNav = computed(() => {
return headers.value.length > 0
})
onContentUpdated(() => {
headers.value = getHeaders(frontmatter.value.outline ?? theme.value.outline)
})
return {
headers,
hasLocalNav
}
}

@ -181,7 +181,7 @@ export function useActiveAnchor(
if (activeLink) { if (activeLink) {
activeLink.classList.add('active') activeLink.classList.add('active')
marker.value.style.top = activeLink.offsetTop + 33 + 'px' marker.value.style.top = activeLink.offsetTop + 39 + 'px'
marker.value.style.opacity = '1' marker.value.style.opacity = '1'
} else { } else {
marker.value.style.top = '33px' marker.value.style.top = '33px'

@ -39,7 +39,6 @@ body {
font-weight: 400; font-weight: 400;
color: var(--vp-c-text-1); color: var(--vp-c-text-1);
background-color: var(--vp-c-bg); background-color: var(--vp-c-bg);
direction: ltr;
font-synthesis: style; font-synthesis: style;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;

@ -264,6 +264,12 @@
--vp-z-index-sidebar: 60; --vp-z-index-sidebar: 60;
} }
@media (min-width: 960px) {
:root {
--vp-z-index-sidebar: 25;
}
}
/** /**
* Icons * Icons
* -------------------------------------------------------------------------- */ * -------------------------------------------------------------------------- */

@ -27,6 +27,7 @@ export { default as VPTeamPageSection } from './components/VPTeamPageSection.vue
export { default as VPTeamMembers } from './components/VPTeamMembers.vue' export { default as VPTeamMembers } from './components/VPTeamMembers.vue'
export { useSidebar } from './composables/sidebar' export { useSidebar } from './composables/sidebar'
export { useLocalNav } from './composables/local-nav'
const theme: Theme = { const theme: Theme = {
Layout, Layout,

@ -104,7 +104,8 @@ export async function resolveConfig(
const { pages, dynamicRoutes, rewrites } = await resolvePages( const { pages, dynamicRoutes, rewrites } = await resolvePages(
srcDir, srcDir,
userConfig userConfig,
logger
) )
const config: SiteConfig = { const config: SiteConfig = {
@ -251,7 +252,7 @@ export async function resolveSiteData(
appearance: userConfig.appearance ?? true, appearance: userConfig.appearance ?? true,
themeConfig: userConfig.themeConfig || {}, themeConfig: userConfig.themeConfig || {},
locales: userConfig.locales || {}, locales: userConfig.locales || {},
scrollOffset: userConfig.scrollOffset ?? 90, scrollOffset: userConfig.scrollOffset ?? 134,
cleanUrls: !!userConfig.cleanUrls, cleanUrls: !!userConfig.cleanUrls,
contentProps: userConfig.contentProps contentProps: userConfig.contentProps
} }

@ -19,31 +19,31 @@ import anchorPlugin from 'markdown-it-anchor'
import attrsPlugin from 'markdown-it-attrs' import attrsPlugin from 'markdown-it-attrs'
// @ts-ignore // @ts-ignore
import { full as emojiPlugin } from 'markdown-it-emoji' import { full as emojiPlugin } from 'markdown-it-emoji'
import type {
BuiltinTheme,
Highlighter,
LanguageInput,
ShikijiTransformer,
ThemeRegistrationAny
} from 'shikiji'
import type { Logger } from 'vite' import type { Logger } from 'vite'
import { containerPlugin, type ContainerOptions } from './plugins/containers' import { containerPlugin, type ContainerOptions } from './plugins/containers'
import { highlight } from './plugins/highlight' import { highlight } from './plugins/highlight'
import { highlightLinePlugin } from './plugins/highlightLines' import { highlightLinePlugin } from './plugins/highlightLines'
import { imagePlugin } from './plugins/image' import { imagePlugin, type Options as ImageOptions } from './plugins/image'
import { lineNumberPlugin } from './plugins/lineNumbers' import { lineNumberPlugin } from './plugins/lineNumbers'
import { linkPlugin } from './plugins/link' import { linkPlugin } from './plugins/link'
import { preWrapperPlugin } from './plugins/preWrapper' import { preWrapperPlugin } from './plugins/preWrapper'
import { snippetPlugin } from './plugins/snippet' import { snippetPlugin } from './plugins/snippet'
import type {
ThemeRegistration,
BuiltinTheme,
LanguageInput,
ShikijiTransformer,
Highlighter
} from 'shikiji'
export type { Header } from '../shared' export type { Header } from '../shared'
export type ThemeOptions = export type ThemeOptions =
| ThemeRegistration | ThemeRegistrationAny
| BuiltinTheme | BuiltinTheme
| { | {
light: ThemeRegistration | BuiltinTheme light: ThemeRegistrationAny | BuiltinTheme
dark: ThemeRegistration | BuiltinTheme dark: ThemeRegistrationAny | BuiltinTheme
} }
export interface MarkdownOptions extends MarkdownIt.Options { export interface MarkdownOptions extends MarkdownIt.Options {
@ -166,6 +166,7 @@ export interface MarkdownOptions extends MarkdownIt.Options {
* @see https://vitepress.dev/guide/markdown#math-equations * @see https://vitepress.dev/guide/markdown#math-equations
*/ */
math?: boolean | any math?: boolean | any
image?: ImageOptions
} }
export type MarkdownRenderer = MarkdownIt export type MarkdownRenderer = MarkdownIt
@ -198,7 +199,7 @@ export const createMarkdownRenderer = async (
.use(preWrapperPlugin, { hasSingleTheme }) .use(preWrapperPlugin, { hasSingleTheme })
.use(snippetPlugin, srcDir) .use(snippetPlugin, srcDir)
.use(containerPlugin, { hasSingleTheme }, options.container) .use(containerPlugin, { hasSingleTheme }, options.container)
.use(imagePlugin) .use(imagePlugin, options.image)
.use( .use(
linkPlugin, linkPlugin,
{ target: '_blank', rel: 'noreferrer', ...options.externalLinks }, { target: '_blank', rel: 'noreferrer', ...options.externalLinks },

@ -2,14 +2,12 @@ import { customAlphabet } from 'nanoid'
import c from 'picocolors' import c from 'picocolors'
import type { ShikijiTransformer } from 'shikiji' import type { ShikijiTransformer } from 'shikiji'
import { import {
addClassToHast,
bundledLanguages, bundledLanguages,
getHighlighter, getHighlighter,
addClassToHast,
isPlaintext as isPlainLang, isPlaintext as isPlainLang,
isSpecialLang isSpecialLang
} from 'shikiji' } from 'shikiji'
import type { Logger } from 'vite'
import type { MarkdownOptions, ThemeOptions } from '../markdown'
import { import {
transformerCompactLineOptions, transformerCompactLineOptions,
transformerNotationDiff, transformerNotationDiff,
@ -18,6 +16,8 @@ import {
transformerNotationHighlight, transformerNotationHighlight,
type TransformerCompactLineOption type TransformerCompactLineOption
} from 'shikiji-transformers' } from 'shikiji-transformers'
import type { Logger } from 'vite'
import type { MarkdownOptions, ThemeOptions } from '../markdown'
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10) const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10)
@ -65,9 +65,9 @@ export async function highlight(
const highlighter = await getHighlighter({ const highlighter = await getHighlighter({
themes: themes:
typeof theme === 'string' || 'name' in theme typeof theme === 'object' && 'light' in theme && 'dark' in theme
? [theme] ? [theme.light, theme.dark]
: [theme.light, theme.dark], : [theme],
langs: [...Object.keys(bundledLanguages), ...(options.languages || [])], langs: [...Object.keys(bundledLanguages), ...(options.languages || [])],
langAlias: options.languageAlias langAlias: options.languageAlias
}) })
@ -169,15 +169,10 @@ export async function highlight(
}, },
...userTransformers ...userTransformers
], ],
meta: { meta: { __raw: attrs },
__raw: attrs ...(typeof theme === 'object' && 'light' in theme && 'dark' in theme
}, ? { themes: theme, defaultColor: false }
...(typeof theme === 'string' || 'name' in theme : { theme })
? { theme }
: {
themes: theme,
defaultColor: false
})
}) })
return fillEmptyHighlightedLine(restoreMustache(highlighted)) return fillEmptyHighlightedLine(restoreMustache(highlighted))

@ -3,7 +3,15 @@
import type MarkdownIt from 'markdown-it' import type MarkdownIt from 'markdown-it'
import { EXTERNAL_URL_RE } from '../../shared' import { EXTERNAL_URL_RE } from '../../shared'
export const imagePlugin = (md: MarkdownIt) => { export interface Options {
/**
* Support native lazy loading for the `<img>` tag.
* @default false
*/
lazyLoading?: boolean
}
export const imagePlugin = (md: MarkdownIt, { lazyLoading }: Options = {}) => {
const imageRule = md.renderer.rules.image! const imageRule = md.renderer.rules.image!
md.renderer.rules.image = (tokens, idx, options, env, self) => { md.renderer.rules.image = (tokens, idx, options, env, self) => {
const token = tokens[idx] const token = tokens[idx]
@ -12,6 +20,9 @@ export const imagePlugin = (md: MarkdownIt) => {
if (!/^\.?\//.test(url)) url = './' + url if (!/^\.?\//.test(url)) url = './' + url
token.attrSet('src', decodeURIComponent(url)) token.attrSet('src', decodeURIComponent(url))
} }
if (lazyLoading) {
token.attrSet('loading', 'lazy')
}
return imageRule(tokens, idx, options, env, self) return imageRule(tokens, idx, options, env, self)
} }
} }

@ -268,7 +268,11 @@ export async function createVitePressPlugin(
if (file.endsWith('.md')) { if (file.endsWith('.md')) {
Object.assign( Object.assign(
siteConfig, siteConfig,
await resolvePages(siteConfig.srcDir, siteConfig.userConfig) await resolvePages(
siteConfig.srcDir,
siteConfig.userConfig,
siteConfig.logger
)
) )
} }

@ -1,6 +1,7 @@
import { import {
loadConfigFromFile, loadConfigFromFile,
normalizePath, normalizePath,
type Logger,
type Plugin, type Plugin,
type ViteDevServer type ViteDevServer
} from 'vite' } from 'vite'
@ -13,7 +14,11 @@ import { resolveRewrites } from './rewritesPlugin'
export const dynamicRouteRE = /\[(\w+?)\]/g export const dynamicRouteRE = /\[(\w+?)\]/g
export async function resolvePages(srcDir: string, userConfig: UserConfig) { export async function resolvePages(
srcDir: string,
userConfig: UserConfig,
logger: Logger
) {
// Important: fast-glob doesn't guarantee order of the returned files. // Important: fast-glob doesn't guarantee order of the returned files.
// We must sort the pages so the input list to rollup is stable across // We must sort the pages so the input list to rollup is stable across
// builds - otherwise different input order could result in different exports // builds - otherwise different input order could result in different exports
@ -39,7 +44,11 @@ export async function resolvePages(srcDir: string, userConfig: UserConfig) {
;(dynamicRouteRE.test(file) ? dynamicRouteFiles : pages).push(file) ;(dynamicRouteRE.test(file) ? dynamicRouteFiles : pages).push(file)
}) })
const dynamicRoutes = await resolveDynamicRoutes(srcDir, dynamicRouteFiles) const dynamicRoutes = await resolveDynamicRoutes(
srcDir,
dynamicRouteFiles,
logger
)
pages.push(...dynamicRoutes.routes.map((r) => r.path)) pages.push(...dynamicRoutes.routes.map((r) => r.path))
const rewrites = resolveRewrites(pages, userConfig.rewrites) const rewrites = resolveRewrites(pages, userConfig.rewrites)
@ -141,7 +150,7 @@ export const dynamicRoutesPlugin = async (
if (!/\.md$/.test(ctx.file)) { if (!/\.md$/.test(ctx.file)) {
Object.assign( Object.assign(
config, config,
await resolvePages(config.srcDir, config.userConfig) await resolvePages(config.srcDir, config.userConfig, config.logger)
) )
} }
for (const id of mods) { for (const id of mods) {
@ -154,7 +163,8 @@ export const dynamicRoutesPlugin = async (
export async function resolveDynamicRoutes( export async function resolveDynamicRoutes(
srcDir: string, srcDir: string,
routes: string[] routes: string[],
logger: Logger
): Promise<SiteConfig['dynamicRoutes']> { ): Promise<SiteConfig['dynamicRoutes']> {
const pendingResolveRoutes: Promise<ResolvedRouteConfig[]>[] = [] const pendingResolveRoutes: Promise<ResolvedRouteConfig[]>[] = []
const routeFileToModulesMap: Record<string, Set<string>> = {} const routeFileToModulesMap: Record<string, Set<string>> = {}
@ -170,7 +180,7 @@ export async function resolveDynamicRoutes(
const pathsFile = paths.find((p) => fs.existsSync(p)) const pathsFile = paths.find((p) => fs.existsSync(p))
if (pathsFile == null) { if (pathsFile == null) {
console.warn( logger.warn(
c.yellow( c.yellow(
`Missing paths file for dynamic route ${route}: ` + `Missing paths file for dynamic route ${route}: ` +
`a corresponding ${paths[0]} (or .ts/.mjs/.mts) file is needed.` `a corresponding ${paths[0]} (or .ts/.mjs/.mts) file is needed.`
@ -183,15 +193,15 @@ export async function resolveDynamicRoutes(
let mod = routeModuleCache.get(pathsFile) let mod = routeModuleCache.get(pathsFile)
if (!mod) { if (!mod) {
try { try {
mod = (await loadConfigFromFile({} as any, pathsFile)) as RouteModule mod = (await loadConfigFromFile(
{} as any,
pathsFile,
undefined,
'silent'
)) as RouteModule
routeModuleCache.set(pathsFile, mod) routeModuleCache.set(pathsFile, mod)
} catch (e) { } catch (e: any) {
console.warn( logger.warn(`${c.yellow(`Failed to load ${pathsFile}:`)}\n${e.stack}`)
c.yellow(
`Invalid paths file export in ${pathsFile}. ` +
`Expects default export of an object with a "paths" property.`
)
)
continue continue
} }
} }
@ -210,7 +220,7 @@ export async function resolveDynamicRoutes(
const loader = mod!.config.paths const loader = mod!.config.paths
if (!loader) { if (!loader) {
console.warn( logger.warn(
c.yellow( c.yellow(
`Invalid paths file export in ${pathsFile}. ` + `Invalid paths file export in ${pathsFile}. ` +
`Missing "paths" property from default export.` `Missing "paths" property from default export.`

1
theme.d.ts vendored

@ -13,6 +13,7 @@ declare const theme: {
export default theme export default theme
export declare const useSidebar: () => DefaultTheme.DocSidebar export declare const useSidebar: () => DefaultTheme.DocSidebar
export declare const useLocalNav: () => DefaultTheme.DocLocalNav
// TODO: add props for these // TODO: add props for these
export declare const VPButton: DefineComponent export declare const VPButton: DefineComponent

@ -99,6 +99,16 @@ export namespace DefaultTheme {
*/ */
darkModeSwitchLabel?: string darkModeSwitchLabel?: string
/**
* @default 'Switch to light theme'
*/
lightModeSwitchTitle?: string
/**
* @default 'Switch to dark theme'
*/
darkModeSwitchTitle?: string
/** /**
* @default 'Menu' * @default 'Menu'
*/ */
@ -351,6 +361,25 @@ export namespace DefaultTheme {
actionText?: string actionText?: string
} }
// local nav -----------------------------------------------------------------
/**
* ReturnType of `useLocalNav`.
*/
export interface DocLocalNav {
/**
* The outline headers of the current page.
*/
headers: ShallowRef<MenuItem[]>
/**
* Whether the current page has a local nav. Local nav is shown when the
* "outline" is present in the page. However, note that the actual
* local nav visibility depends on the screen width as well.
*/
hasLocalNav: ComputedRef<boolean>
}
// outline ------------------------------------------------------------------- // outline -------------------------------------------------------------------
export interface Outline { export interface Outline {

Loading…
Cancel
Save