Skip to content

Commit fa87d81

Browse files
userquinbrc-dd
andauthored
feat: allow using components in navigation bar (#4000)
--------- Co-authored-by: Divyansh Singh <[email protected]>
1 parent fa81e89 commit fa87d81

File tree

13 files changed

+969
-437
lines changed

13 files changed

+969
-437
lines changed

Diff for: __tests__/e2e/.vitepress/config.ts

+46
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,50 @@
11
import { defineConfig, type DefaultTheme } from 'vitepress'
22

3+
const nav: DefaultTheme.Config['nav'] = [
4+
{
5+
text: 'Home',
6+
link: '/'
7+
},
8+
{
9+
text: 'API Reference',
10+
items: [
11+
{
12+
text: 'Example',
13+
link: '/home.html'
14+
},
15+
{
16+
component: 'ApiPreference',
17+
props: {
18+
options: ['JavaScript', 'TypeScript', 'Flow'],
19+
defaultOption: 'TypeScript'
20+
}
21+
},
22+
{
23+
component: 'ApiPreference',
24+
props: {
25+
options: ['Options', 'Composition'],
26+
defaultOption: 'Composition'
27+
}
28+
}
29+
]
30+
},
31+
{
32+
component: 'NavVersion',
33+
props: {
34+
versions: [
35+
{
36+
text: 'v1.x',
37+
link: '/'
38+
},
39+
{
40+
text: 'v0.x',
41+
link: '/v0.x/'
42+
}
43+
]
44+
}
45+
}
46+
]
47+
348
const sidebar: DefaultTheme.Config['sidebar'] = {
449
'/': [
550
{
@@ -92,6 +137,7 @@ export default defineConfig({
92137
}
93138
},
94139
themeConfig: {
140+
nav,
95141
sidebar,
96142
search: {
97143
provider: 'local',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<script setup lang="ts">
2+
import { useLocalStorage } from '@vueuse/core'
3+
4+
const props = defineProps<{
5+
options: string[]
6+
defaultOption: string
7+
screenMenu?: boolean
8+
}>()
9+
10+
// reactivity isn't needed for props here
11+
12+
const key = removeSpaces(`api-preference-${props.options.join('-')}`)
13+
const name = key + (props.screenMenu ? '-screen-menu' : '')
14+
15+
const selected = useLocalStorage(key, () => props.defaultOption)
16+
17+
const optionsWithKeys = props.options.map((option) => ({
18+
key: name + '-' + removeSpaces(option),
19+
value: option
20+
}))
21+
22+
function removeSpaces(str: string) {
23+
return str.replace(/\s/g, '_')
24+
}
25+
</script>
26+
27+
<template>
28+
<div class="VPApiPreference" :class="{ 'screen-menu': screenMenu }">
29+
<template v-for="option in optionsWithKeys" :key="option">
30+
<input
31+
type="radio"
32+
:id="option.key"
33+
:name="name"
34+
:value="option.value"
35+
v-model="selected"
36+
/>
37+
<label :for="option.key">{{ option.value }}</label>
38+
</template>
39+
</div>
40+
</template>
41+
42+
<style scoped>
43+
.VPApiPreference {
44+
display: flex;
45+
margin: 12px 0;
46+
border: 1px solid var(--vp-c-border);
47+
border-radius: 6px;
48+
font-size: 14px;
49+
color: var(--vp-c-text-1);
50+
}
51+
52+
.VPApiPreference:first-child {
53+
margin-top: 0;
54+
}
55+
56+
.VPApiPreference:last-child {
57+
margin-bottom: 0;
58+
}
59+
60+
.VPApiPreference.screen-menu {
61+
margin: 12px 0 0 12px;
62+
}
63+
64+
.VPApiPreference input[type='radio'] {
65+
pointer-events: none;
66+
position: fixed;
67+
opacity: 0;
68+
}
69+
70+
.VPApiPreference label {
71+
flex: 1;
72+
margin: 2px;
73+
padding: 4px 12px;
74+
cursor: pointer;
75+
border-radius: 4px;
76+
text-align: center;
77+
}
78+
79+
.VPApiPreference input[type='radio']:checked + label {
80+
background-color: var(--vp-c-default-soft);
81+
color: var(--vp-c-brand-1);
82+
}
83+
</style>
+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<script setup lang="ts">
2+
import { computed } from 'vue'
3+
import { useRoute } from 'vitepress'
4+
import VPNavBarMenuGroup from 'vitepress/dist/client/theme-default/components/VPNavBarMenuGroup.vue'
5+
import VPNavScreenMenuGroup from 'vitepress/dist/client/theme-default/components/VPNavScreenMenuGroup.vue'
6+
7+
const props = defineProps<{
8+
versions: { text: string; link: string }[]
9+
screenMenu?: boolean
10+
}>()
11+
12+
const route = useRoute()
13+
14+
const sortedVersions = computed(() => {
15+
return [...props.versions].sort(
16+
(a, b) => b.link.split('/').length - a.link.split('/').length
17+
)
18+
})
19+
20+
const currentVersion = computed(() => {
21+
return (
22+
sortedVersions.value.find((version) => route.path.startsWith(version.link))
23+
?.text || 'Versions'
24+
)
25+
})
26+
</script>
27+
28+
<template>
29+
<VPNavBarMenuGroup
30+
v-if="!screenMenu"
31+
:item="{ text: currentVersion, items: versions }"
32+
class="VPNavVersion"
33+
/>
34+
<VPNavScreenMenuGroup
35+
v-else
36+
:text="currentVersion"
37+
:items="versions"
38+
class="VPNavVersion"
39+
/>
40+
</template>
41+
42+
<style scoped>
43+
.VPNavVersion :deep(button .text) {
44+
color: var(--vp-c-text-1) !important;
45+
}
46+
47+
.VPNavVersion:hover :deep(button .text) {
48+
color: var(--vp-c-text-2) !important;
49+
}
50+
</style>

Diff for: __tests__/e2e/.vitepress/theme/index.ts

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type { Theme } from 'vitepress'
2+
import DefaultTheme from 'vitepress/theme'
3+
import ApiPreference from './components/ApiPreference.vue'
4+
import NavVersion from './components/NavVersion.vue'
5+
6+
export default {
7+
extends: DefaultTheme,
8+
enhanceApp({ app }) {
9+
app.component('ApiPreference', ApiPreference)
10+
app.component('NavVersion', NavVersion)
11+
}
12+
} satisfies Theme

Diff for: docs/en/reference/default-theme-nav.md

+54
Original file line numberDiff line numberDiff line change
@@ -160,3 +160,57 @@ export default {
160160
## Social Links
161161

162162
Refer [`socialLinks`](./default-theme-config#sociallinks).
163+
164+
## Custom Components
165+
166+
You can include custom components in the navigation bar by using the `component` option. The `component` key should be the Vue component name, and must be registered globally using [Theme.enhanceApp](../guide/custom-theme#theme-interface).
167+
168+
```js
169+
// .vitepress/config.js
170+
export default {
171+
themeConfig: {
172+
nav: [
173+
{
174+
text: 'My Menu',
175+
items: [
176+
{
177+
component: 'MyCustomComponent',
178+
// Optional props to pass to the component
179+
props: {
180+
title: 'My Custom Component'
181+
}
182+
}
183+
]
184+
},
185+
{
186+
component: 'AnotherCustomComponent'
187+
}
188+
]
189+
}
190+
}
191+
```
192+
193+
Then, you need to register the component globally:
194+
195+
```js
196+
// .vitepress/theme/index.js
197+
import DefaultTheme from 'vitepress/theme'
198+
199+
import MyCustomComponent from './components/MyCustomComponent.vue'
200+
import AnotherCustomComponent from './components/AnotherCustomComponent.vue'
201+
202+
/** @type {import('vitepress').Theme} */
203+
export default {
204+
extends: DefaultTheme,
205+
enhanceApp({ app }) {
206+
app.component('MyCustomComponent', MyCustomComponent)
207+
app.component('AnotherCustomComponent', AnotherCustomComponent)
208+
}
209+
}
210+
```
211+
212+
Your component will be rendered in the navigation bar. VitePress will provide the following additional props to the component:
213+
214+
- `screenMenu`: an optional boolean indicating whether the component is inside mobile navigation menu
215+
216+
You can check an example in the e2e tests [here](https://github.com/vuejs/vitepress/tree/main/__tests__/e2e/.vitepress).

Diff for: package.json

+15-15
Original file line numberDiff line numberDiff line change
@@ -100,20 +100,20 @@
100100
"dependencies": {
101101
"@docsearch/css": "^3.6.0",
102102
"@docsearch/js": "^3.6.0",
103-
"@shikijs/core": "^1.9.0",
104-
"@shikijs/transformers": "^1.9.0",
103+
"@shikijs/core": "^1.10.3",
104+
"@shikijs/transformers": "^1.10.3",
105105
"@types/markdown-it": "^14.1.1",
106106
"@vitejs/plugin-vue": "^5.0.5",
107-
"@vue/devtools-api": "^7.3.4",
108-
"@vue/shared": "^3.4.30",
107+
"@vue/devtools-api": "^7.3.5",
108+
"@vue/shared": "^3.4.31",
109109
"@vueuse/core": "^10.11.0",
110110
"@vueuse/integrations": "^10.11.0",
111111
"focus-trap": "^7.5.4",
112112
"mark.js": "8.11.1",
113113
"minisearch": "^6.3.0",
114-
"shiki": "^1.9.0",
115-
"vite": "^5.3.1",
116-
"vue": "^3.4.30"
114+
"shiki": "^1.10.3",
115+
"vite": "^5.3.3",
116+
"vue": "^3.4.31"
117117
},
118118
"devDependencies": {
119119
"@clack/prompts": "^0.7.0",
@@ -138,24 +138,24 @@
138138
"@types/markdown-it-attrs": "^4.1.3",
139139
"@types/markdown-it-container": "^2.0.10",
140140
"@types/markdown-it-emoji": "^3.0.1",
141-
"@types/micromatch": "^4.0.7",
141+
"@types/micromatch": "^4.0.9",
142142
"@types/minimist": "^1.2.5",
143-
"@types/node": "^20.14.8",
143+
"@types/node": "^20.14.10",
144144
"@types/postcss-prefix-selector": "^1.16.3",
145145
"@types/prompts": "^2.4.9",
146146
"chokidar": "^3.6.0",
147147
"conventional-changelog-cli": "^5.0.0",
148148
"cross-spawn": "^7.0.3",
149149
"debug": "^4.3.5",
150-
"esbuild": "^0.21.5",
150+
"esbuild": "^0.23.0",
151151
"execa": "^9.3.0",
152152
"fast-glob": "^3.3.2",
153153
"fs-extra": "^11.2.0",
154154
"get-port": "^7.1.0",
155155
"gray-matter": "^4.0.3",
156156
"lint-staged": "^15.2.7",
157157
"lodash.template": "^4.5.0",
158-
"lru-cache": "^10.2.2",
158+
"lru-cache": "^10.3.1",
159159
"markdown-it": "^14.1.0",
160160
"markdown-it-anchor": "^9.0.1",
161161
"markdown-it-attrs": "^4.1.6",
@@ -170,13 +170,13 @@
170170
"path-to-regexp": "^6.2.2",
171171
"picocolors": "^1.0.1",
172172
"pkg-dir": "^8.0.0",
173-
"playwright-chromium": "^1.44.1",
173+
"playwright-chromium": "^1.45.1",
174174
"polka": "^1.0.0-next.25",
175175
"postcss-prefix-selector": "^1.16.1",
176176
"prettier": "^3.3.2",
177177
"prompts": "^2.4.2",
178178
"punycode": "^2.3.1",
179-
"rimraf": "^5.0.7",
179+
"rimraf": "^5.0.8",
180180
"rollup": "^4.18.0",
181181
"rollup-plugin-dts": "^6.1.1",
182182
"rollup-plugin-esbuild": "^6.1.1",
@@ -185,9 +185,9 @@
185185
"sirv": "^2.0.4",
186186
"sitemap": "^8.0.0",
187187
"supports-color": "^9.4.0",
188-
"typescript": "^5.5.2",
188+
"typescript": "^5.5.3",
189189
"vitest": "^1.6.0",
190-
"vue-tsc": "^2.0.22",
190+
"vue-tsc": "^2.0.26",
191191
"wait-on": "^7.2.0"
192192
},
193193
"peerDependencies": {

0 commit comments

Comments
 (0)