Skip to content

Commit 7182c42

Browse files
committed
feat(theme): page outline for mobile
1 parent 15a2dd2 commit 7182c42

File tree

8 files changed

+229
-55
lines changed

8 files changed

+229
-55
lines changed

docs/reference/site-config.md

-14
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,6 @@ outline: deep
66

77
Site config is where you can define the global settings of the site. App config options define settings that apply to every VitePress site, regardless of what theme it is using. For example, the base directory or the title of the site.
88

9-
<div class="site-config-toc">
10-
11-
[[toc]]
12-
13-
</div>
14-
15-
<style>
16-
@media (min-width: 1280px) {
17-
.site-config-toc {
18-
display: none;
19-
}
20-
}
21-
</style>
22-
239
## Overview
2410

2511
### Config Resolution

src/client/theme-default/components/VPDoc.vue

+12
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { computed } from 'vue'
44
import { useSidebar } from '../composables/sidebar.js'
55
import VPDocAside from './VPDocAside.vue'
66
import VPDocFooter from './VPDocFooter.vue'
7+
import VPDocOutlineDropdown from './VPDocOutlineDropdown.vue'
78
89
const route = useRoute()
910
const { hasSidebar, hasAside } = useSidebar()
@@ -38,6 +39,7 @@ const pageName = computed(() =>
3839
<div class="content">
3940
<div class="content-container">
4041
<slot name="doc-before" />
42+
<VPDocOutlineDropdown />
4143
<main class="main">
4244
<Content class="vp-doc" :class="pageName" />
4345
</main>
@@ -56,6 +58,16 @@ const pageName = computed(() =>
5658
width: 100%;
5759
}
5860
61+
.VPDoc .VPDocOutlineDropdown {
62+
display: none;
63+
}
64+
65+
@media (min-width: 768px) and (max-width: 1280px) {
66+
.VPDoc .VPDocOutlineDropdown {
67+
display: block;
68+
}
69+
}
70+
5971
@media (min-width: 768px) {
6072
.VPDoc {
6173
padding: 48px 32px 128px;

src/client/theme-default/components/VPDocAsideOutline.vue

+4-19
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ import { ref, shallowRef } from 'vue'
33
import { useData } from '../composables/data.js'
44
import {
55
getHeaders,
6+
resolveTitle,
67
useActiveAnchor,
78
type MenuItem
89
} from '../composables/outline.js'
9-
import VPDocAsideOutlineItem from './VPDocAsideOutlineItem.vue'
10+
import VPDocOutlineItem from './VPDocOutlineItem.vue'
1011
import { onContentUpdated } from 'vitepress'
1112
1213
const { frontmatter, theme } = useData()
@@ -23,36 +24,20 @@ const container = ref()
2324
const marker = ref()
2425
2526
useActiveAnchor(container, marker)
26-
27-
function handleClick({ target: el }: Event) {
28-
const id = '#' + (el as HTMLAnchorElement).href!.split('#')[1]
29-
const heading = document.querySelector<HTMLAnchorElement>(
30-
decodeURIComponent(id)
31-
)
32-
heading?.focus()
33-
}
3427
</script>
3528

3629
<template>
3730
<div class="VPDocAsideOutline" :class="{ 'has-outline': headers.length > 0 }" ref="container">
3831
<div class="content">
3932
<div class="outline-marker" ref="marker" />
4033

41-
<div class="outline-title">
42-
{{
43-
(typeof theme.outline === 'object' &&
44-
!Array.isArray(theme.outline) &&
45-
theme.outline.label) ||
46-
theme.outlineTitle ||
47-
'On this page'
48-
}}
49-
</div>
34+
<div class="outline-title">{{ resolveTitle(theme) }}</div>
5035

5136
<nav aria-labelledby="doc-outline-aria-label">
5237
<span class="visually-hidden" id="doc-outline-aria-label">
5338
Table of Contents for current page
5439
</span>
55-
<VPDocAsideOutlineItem :headers="headers" :root="true" :onClick="handleClick" />
40+
<VPDocOutlineItem :headers="headers" :root="true" />
5641
</nav>
5742
</div>
5843
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<script setup lang="ts">
2+
import { ref } from 'vue'
3+
import { useData } from '../composables/data.js'
4+
import { getHeaders, resolveTitle } from '../composables/outline.js'
5+
import VPDocOutlineItem from './VPDocOutlineItem.vue'
6+
import { onContentUpdated } from 'vitepress'
7+
import VPIconChevronRight from './icons/VPIconChevronRight.vue'
8+
9+
const { frontmatter, theme } = useData()
10+
const open = ref(false)
11+
12+
onContentUpdated(() => {
13+
open.value = false
14+
})
15+
</script>
16+
17+
<template>
18+
<div class="VPDocOutlineDropdown">
19+
<button @click="open = !open" :class="{ open }">
20+
{{ resolveTitle(theme) }}
21+
<VPIconChevronRight class="icon" />
22+
</button>
23+
<div class="items" v-if="open">
24+
<VPDocOutlineItem :headers="getHeaders(frontmatter.outline ?? theme.outline)" />
25+
</div>
26+
</div>
27+
</template>
28+
29+
<style scoped>
30+
.VPDocOutlineDropdown {
31+
margin-bottom: 42px;
32+
}
33+
34+
.VPDocOutlineDropdown button {
35+
display: block;
36+
font-size: 14px;
37+
font-weight: 500;
38+
line-height: 24px;
39+
color: var(--vp-c-text-2);
40+
transition: color 0.5s;
41+
border: 1px solid var(--vp-c-border);
42+
padding: 4px 12px;
43+
border-radius: 8px;
44+
}
45+
46+
.VPDocOutlineDropdown button:hover {
47+
color: var(--vp-c-text-1);
48+
transition: color 0.25s;
49+
}
50+
51+
.VPDocOutlineDropdown button.open {
52+
color: var(--vp-c-text-1);
53+
}
54+
55+
.icon {
56+
display: inline-block;
57+
vertical-align: middle;
58+
margin-left: 2px;
59+
width: 14px;
60+
height: 14px;
61+
fill: currentColor;
62+
}
63+
64+
:deep(.outline-link) {
65+
font-size: 13px;
66+
}
67+
68+
.open > .icon {
69+
transform: rotate(90deg);
70+
}
71+
72+
.items {
73+
margin-top: 10px;
74+
border-left: 1px solid var(--vp-c-divider);
75+
}
76+
</style>

src/client/theme-default/components/VPDocAsideOutlineItem.vue src/client/theme-default/components/VPDocOutlineItem.vue

+10-2
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,24 @@ import type { MenuItem } from '../composables/outline.js'
33
44
defineProps<{
55
headers: MenuItem[]
6-
onClick: (e: MouseEvent) => void
76
root?: boolean
87
}>()
8+
9+
function onClick({ target: el }: Event) {
10+
const id = '#' + (el as HTMLAnchorElement).href!.split('#')[1]
11+
const heading = document.querySelector<HTMLAnchorElement>(
12+
decodeURIComponent(id)
13+
)
14+
heading?.focus()
15+
}
916
</script>
1017

1118
<template>
1219
<ul :class="root ? 'root' : 'nested'">
1320
<li v-for="{ children, link, title } in headers">
1421
<a class="outline-link" :href="link" @click="onClick">{{ title }}</a>
1522
<template v-if="children?.length">
16-
<VPDocAsideOutlineItem :headers="children" :onClick="onClick" />
23+
<VPDocOutlineItem :headers="children" />
1724
</template>
1825
</li>
1926
</ul>
@@ -37,6 +44,7 @@ defineProps<{
3744
overflow: hidden;
3845
text-overflow: ellipsis;
3946
transition: color 0.5s;
47+
font-weight: 500;
4048
}
4149
4250
.outline-link:hover,

src/client/theme-default/components/VPLocalNav.vue

+4-20
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { useData } from '../composables/data.js'
33
import { useSidebar } from '../composables/sidebar.js'
44
import VPIconAlignLeft from './icons/VPIconAlignLeft.vue'
5+
import VPLocalNavOutlineDropdown from './VPLocalNavOutlineDropdown.vue'
56
67
defineProps<{
78
open: boolean
@@ -13,10 +14,6 @@ defineEmits<{
1314
1415
const { theme } = useData()
1516
const { hasSidebar } = useSidebar()
16-
17-
function scrollToTop() {
18-
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
19-
}
2017
</script>
2118

2219
<template>
@@ -33,9 +30,7 @@ function scrollToTop() {
3330
</span>
3431
</button>
3532

36-
<a class="top-link" href="#" @click="scrollToTop">
37-
{{ theme.returnToTopLabel || 'Return to top' }}
38-
</a>
33+
<VPLocalNavOutlineDropdown />
3934
</div>
4035
</template>
4136

@@ -91,23 +86,12 @@ function scrollToTop() {
9186
fill: currentColor;
9287
}
9388
94-
.top-link {
95-
display: block;
89+
.VPOutlineDropdown {
9690
padding: 12px 24px 11px;
97-
line-height: 24px;
98-
font-size: 12px;
99-
font-weight: 500;
100-
color: var(--vp-c-text-2);
101-
transition: color 0.5s;
102-
}
103-
104-
.top-link:hover {
105-
color: var(--vp-c-text-1);
106-
transition: color 0.25s;
10791
}
10892
10993
@media (min-width: 768px) {
110-
.top-link {
94+
.VPOutlineDropdown {
11195
padding: 12px 32px 11px;
11296
}
11397
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
<script setup lang="ts">
2+
import { ref } from 'vue'
3+
import { useData } from '../composables/data.js'
4+
import { getHeaders, resolveTitle } from '../composables/outline.js'
5+
import VPDocOutlineItem from './VPDocOutlineItem.vue'
6+
import { onContentUpdated } from 'vitepress'
7+
import VPIconChevronRight from './icons/VPIconChevronRight.vue'
8+
9+
const { frontmatter, theme } = useData()
10+
const open = ref(false)
11+
const vh = ref(0)
12+
13+
onContentUpdated(() => {
14+
open.value = false
15+
})
16+
17+
function toggle() {
18+
open.value = !open.value
19+
vh.value = window.innerHeight + Math.min(window.scrollY - 64, 0)
20+
}
21+
22+
function onItemClick(e: Event) {
23+
if ((e.target as HTMLElement).classList.contains('outline-link')) {
24+
open.value = false
25+
}
26+
}
27+
28+
function scrollToTop() {
29+
open.value = false
30+
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
31+
}
32+
</script>
33+
34+
<template>
35+
<div class="VPLocalNavOutlineDropdown" :style="{ '--vp-vh': vh + 'px' }">
36+
<button @click="toggle" :class="{ open }">
37+
{{ resolveTitle(theme) }}
38+
<VPIconChevronRight class="icon" />
39+
</button>
40+
<div class="items" v-if="open" @click="onItemClick">
41+
<a class="top-link" href="#" @click="scrollToTop">
42+
{{ theme.returnToTopLabel || 'Return to top' }}
43+
</a>
44+
<VPDocOutlineItem :headers="getHeaders(frontmatter.outline ?? theme.outline)" />
45+
</div>
46+
</div>
47+
</template>
48+
49+
<style scoped>
50+
.VPLocalNavOutlineDropdown {
51+
padding: 12px 20px 11px;
52+
}
53+
.VPLocalNavOutlineDropdown button {
54+
display: block;
55+
font-size: 12px;
56+
font-weight: 500;
57+
line-height: 24px;
58+
color: var(--vp-c-text-2);
59+
transition: color 0.5s;
60+
position: relative;
61+
}
62+
63+
.VPLocalNavOutlineDropdown button:hover {
64+
color: var(--vp-c-text-1);
65+
transition: color 0.25s;
66+
}
67+
68+
.VPLocalNavOutlineDropdown button.open {
69+
color: var(--vp-c-text-1);
70+
}
71+
72+
.icon {
73+
display: inline-block;
74+
vertical-align: middle;
75+
margin-left: 2px;
76+
width: 14px;
77+
height: 14px;
78+
fill: currentColor;
79+
}
80+
81+
:deep(.outline-link) {
82+
font-size: 14px;
83+
padding: 2px 0;
84+
}
85+
86+
.open > .icon {
87+
transform: rotate(90deg);
88+
}
89+
90+
.items {
91+
position: absolute;
92+
left: 20px;
93+
right: 20px;
94+
top: 64px;
95+
background-color: var(--vp-local-nav-bg-color);
96+
padding: 4px 10px 16px;
97+
border: 1px solid var(--vp-c-divider);
98+
border-radius: 8px;
99+
max-height: calc(var(--vp-vh, 100vh) - 86px);
100+
overflow: scroll;
101+
box-shadow: var(--vp-shadow-3);
102+
}
103+
104+
.top-link {
105+
display: block;
106+
color: var(--vp-c-brand);
107+
font-size: 13px;
108+
font-weight: 500;
109+
padding: 6px 0;
110+
margin: 0 13px 10px;
111+
border-bottom: 1px solid var(--vp-c-divider);
112+
}
113+
</style>

0 commit comments

Comments
 (0)