vitepress-theme-element-plus 1.3.2 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/components/Doc.vue +43 -7
- package/client/components/Footer.vue +2 -1
- package/client/components/Layout.vue +4 -1
- package/client/components/MobilePreviewFrame.vue +194 -0
- package/client/components/MobilePreviewLayout.vue +140 -0
- package/client/mobile-preview.ts +60 -0
- package/index.ts +12 -0
- package/package.json +1 -1
|
@@ -4,11 +4,14 @@ import { computed } from 'vue'
|
|
|
4
4
|
import { useSidebar } from '../hooks/useSidebar'
|
|
5
5
|
import VPDocAside from './DocAside.vue'
|
|
6
6
|
import VPDocFooter from './DocFooter.vue'
|
|
7
|
+
import MobilePreviewFrame from './MobilePreviewFrame.vue'
|
|
7
8
|
|
|
8
|
-
const { theme } = useData()
|
|
9
|
+
const { theme, frontmatter } = useData()
|
|
9
10
|
|
|
10
11
|
const route = useRoute()
|
|
11
12
|
const { hasAside, leftAside, hasSidebar } = useSidebar()
|
|
13
|
+
const hasMobilePreview = computed(() => typeof frontmatter.value.mobileDemo === 'string' && frontmatter.value.mobileDemo.trim().length > 0)
|
|
14
|
+
const showAside = computed(() => hasAside.value)
|
|
12
15
|
|
|
13
16
|
const pageName = computed(() =>
|
|
14
17
|
route.path.replace(/[./]+/g, '_').replace(/_html$/, ''),
|
|
@@ -16,10 +19,16 @@ const pageName = computed(() =>
|
|
|
16
19
|
</script>
|
|
17
20
|
|
|
18
21
|
<template>
|
|
19
|
-
<div class="VPDoc" :class="{ 'has-sidebar': hasSidebar, 'has-aside':
|
|
22
|
+
<div class="VPDoc" :class="{ 'has-sidebar': hasSidebar, 'has-aside': showAside, 'has-mobile-preview': hasMobilePreview }">
|
|
20
23
|
<slot name="doc-top" />
|
|
21
24
|
<div class="container">
|
|
22
|
-
<div v-if="
|
|
25
|
+
<div v-if="hasMobilePreview" class="preview">
|
|
26
|
+
<div class="preview-container">
|
|
27
|
+
<MobilePreviewFrame />
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div v-if="showAside" class="aside" :class="{ 'left-aside': leftAside }">
|
|
23
32
|
<div class="aside-container">
|
|
24
33
|
<div class="aside-content">
|
|
25
34
|
<VPDocAside>
|
|
@@ -99,10 +108,6 @@ const pageName = computed(() =>
|
|
|
99
108
|
}
|
|
100
109
|
|
|
101
110
|
@media (min-width: 1440px) {
|
|
102
|
-
.VPDoc.has-aside {
|
|
103
|
-
padding: 64px 48px 48px 64px;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
111
|
.VPDoc.has-aside {
|
|
107
112
|
padding: 64px 0 48px 64px;
|
|
108
113
|
}
|
|
@@ -112,9 +117,23 @@ const pageName = computed(() =>
|
|
|
112
117
|
justify-content: center;
|
|
113
118
|
}
|
|
114
119
|
|
|
120
|
+
.VPDoc.has-mobile-preview .container {
|
|
121
|
+
gap: 32px;
|
|
122
|
+
}
|
|
123
|
+
|
|
115
124
|
.VPDoc .aside {
|
|
116
125
|
display: block;
|
|
117
126
|
}
|
|
127
|
+
|
|
128
|
+
.VPDoc.has-aside.has-mobile-preview .aside {
|
|
129
|
+
display: none;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
@media (min-width: 1440px) and (max-width: 1679px) {
|
|
134
|
+
.VPDoc.has-aside.has-mobile-preview {
|
|
135
|
+
padding-right: 32px;
|
|
136
|
+
}
|
|
118
137
|
}
|
|
119
138
|
|
|
120
139
|
@media (min-width: 1440px) {
|
|
@@ -132,6 +151,17 @@ const pageName = computed(() =>
|
|
|
132
151
|
width: 100%;
|
|
133
152
|
}
|
|
134
153
|
|
|
154
|
+
.preview {
|
|
155
|
+
margin-bottom: 24px;
|
|
156
|
+
order: 2;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.preview-container {
|
|
160
|
+
width: 100%;
|
|
161
|
+
position: sticky;
|
|
162
|
+
top: calc(var(--vp-nav-height) + 32px);
|
|
163
|
+
}
|
|
164
|
+
|
|
135
165
|
.aside {
|
|
136
166
|
position: relative;
|
|
137
167
|
display: none;
|
|
@@ -179,6 +209,12 @@ const pageName = computed(() =>
|
|
|
179
209
|
}
|
|
180
210
|
}
|
|
181
211
|
|
|
212
|
+
@media (min-width: 1680px) {
|
|
213
|
+
.VPDoc.has-aside.has-mobile-preview .aside {
|
|
214
|
+
display: block;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
182
218
|
.content-container {
|
|
183
219
|
margin: 0 auto;
|
|
184
220
|
}
|
|
@@ -41,7 +41,7 @@ const linkUnderline = computed(() => {
|
|
|
41
41
|
.footer {
|
|
42
42
|
background-color: var(--vp-c-bg-soft);
|
|
43
43
|
box-sizing: border-box;
|
|
44
|
-
padding:
|
|
44
|
+
padding: 0px 64px 64px;
|
|
45
45
|
|
|
46
46
|
&.is-home {
|
|
47
47
|
background-color: var(--bg-color);
|
|
@@ -60,6 +60,7 @@ const linkUnderline = computed(() => {
|
|
|
60
60
|
display: inline-block;
|
|
61
61
|
vertical-align: top;
|
|
62
62
|
margin-right: 130px;
|
|
63
|
+
margin-top: 42px;
|
|
63
64
|
width: 200px;
|
|
64
65
|
|
|
65
66
|
h4 {
|
|
@@ -8,10 +8,12 @@ import { computed, provide, useSlots, watch } from 'vue'
|
|
|
8
8
|
import { useCloseSidebarOnEscape } from '../hooks/useSidebar'
|
|
9
9
|
import Content from './Content.vue'
|
|
10
10
|
import LocalNav from './LocalNav.vue'
|
|
11
|
+
import MobilePreviewLayout from './MobilePreviewLayout.vue'
|
|
11
12
|
import Nav from './Nav.vue'
|
|
12
13
|
import Sidebar from './Sidebar.vue'
|
|
13
14
|
|
|
14
15
|
const { frontmatter } = useData()
|
|
16
|
+
const isStandaloneMobilePreview = computed(() => frontmatter.value.layout === 'mobile-preview')
|
|
15
17
|
|
|
16
18
|
useCloseSidebarOnEscape()
|
|
17
19
|
const {
|
|
@@ -32,7 +34,8 @@ provide(layoutInfoInjectionKey, heroImageSlotExists)
|
|
|
32
34
|
</script>
|
|
33
35
|
|
|
34
36
|
<template>
|
|
35
|
-
<
|
|
37
|
+
<MobilePreviewLayout v-if="isStandaloneMobilePreview" />
|
|
38
|
+
<div v-else-if="frontmatter.layout !== false" class="Layout VMLayout" :class="frontmatter.pageClass">
|
|
36
39
|
<slot name="layout-top" />
|
|
37
40
|
<VPSkipLink />
|
|
38
41
|
<VPBackdrop class="backdrop" :show="isSidebarOpen" @click="closeSidebar()" />
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { SiteData } from 'vitepress'
|
|
3
|
+
import { useData, useRoute } from 'vitepress'
|
|
4
|
+
import { computed, ref, watch } from 'vue'
|
|
5
|
+
import { resolveMobilePreviewId } from '../mobile-preview'
|
|
6
|
+
|
|
7
|
+
const { frontmatter, isDark, site, theme } = useData()
|
|
8
|
+
const route = useRoute()
|
|
9
|
+
const frameRef = ref<HTMLIFrameElement>()
|
|
10
|
+
const previewConfig = computed(() => theme.value.mobilePreview ?? {})
|
|
11
|
+
|
|
12
|
+
const demoId = computed(() => {
|
|
13
|
+
const value = frontmatter.value.mobileDemo
|
|
14
|
+
return typeof value === 'string'
|
|
15
|
+
? resolveMobilePreviewId(value, previewConfig.value.demoRoot)
|
|
16
|
+
: ''
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const frameWidth = computed(() => `${previewConfig.value.deviceWidth ?? 390}px`)
|
|
20
|
+
const viewportHeight = computed(() => `${previewConfig.value.deviceHeight ?? 760}px`)
|
|
21
|
+
|
|
22
|
+
const previewHref = computed(() => {
|
|
23
|
+
const previewPath = normalizePreviewPath(previewConfig.value.previewPath)
|
|
24
|
+
const search = new URLSearchParams({
|
|
25
|
+
demo: demoId.value,
|
|
26
|
+
theme: isDark.value ? 'dark' : 'light',
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
return `${resolveLocalePreviewPath(route.path, previewPath, site.value)}?${search.toString()}`
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const frameStyle = computed(() => ({
|
|
33
|
+
'--vp-mobile-preview-width': frameWidth.value,
|
|
34
|
+
'--vp-mobile-preview-height': viewportHeight.value,
|
|
35
|
+
}))
|
|
36
|
+
|
|
37
|
+
function normalizePreviewPath(value: unknown): string {
|
|
38
|
+
if (typeof value !== 'string' || !value.trim())
|
|
39
|
+
return 'preview/'
|
|
40
|
+
|
|
41
|
+
const trimmedValue = value.trim()
|
|
42
|
+
const normalizedPath = trimmedValue
|
|
43
|
+
.replace(/^\/+/, '')
|
|
44
|
+
.replace(/\/?$/, '/')
|
|
45
|
+
|
|
46
|
+
return trimmedValue.startsWith('/')
|
|
47
|
+
? `/${normalizedPath}`
|
|
48
|
+
: normalizedPath
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function normalizeLocalePrefix(value: string): string {
|
|
52
|
+
return `/${value.replace(/^\/+|\/+$/g, '')}/`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getLocalePrefix(path: string, siteData: SiteData): string {
|
|
56
|
+
const localePrefixes = Object.keys(siteData.locales ?? {})
|
|
57
|
+
.filter(locale => locale !== 'root')
|
|
58
|
+
.map(normalizeLocalePrefix)
|
|
59
|
+
.sort((left, right) => right.length - left.length)
|
|
60
|
+
|
|
61
|
+
return localePrefixes.find(prefix => path.startsWith(prefix)) ?? '/'
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function resolveLocalePreviewPath(path: string, previewPath: string, siteData: SiteData): string {
|
|
65
|
+
if (previewPath.startsWith('/'))
|
|
66
|
+
return previewPath
|
|
67
|
+
|
|
68
|
+
const localePrefix = getLocalePrefix(path, siteData)
|
|
69
|
+
return new URL(previewPath, `https://mobile-preview.local${localePrefix}`).pathname
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function syncTheme(): void {
|
|
73
|
+
frameRef.value?.contentWindow?.postMessage({
|
|
74
|
+
type: 'vp-mobile-preview-theme',
|
|
75
|
+
value: isDark.value ? 'dark' : 'light',
|
|
76
|
+
}, '*')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
watch(isDark, syncTheme)
|
|
80
|
+
</script>
|
|
81
|
+
|
|
82
|
+
<template>
|
|
83
|
+
<section v-if="demoId" class="VPMobilePreviewFrame" :style="frameStyle">
|
|
84
|
+
<div class="preview-actions">
|
|
85
|
+
<span class="preview-actions__label">Mobile Preview</span>
|
|
86
|
+
<a class="preview-actions__link" :href="previewHref" target="_blank" rel="noreferrer">
|
|
87
|
+
Open
|
|
88
|
+
</a>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div class="preview-phone">
|
|
92
|
+
<div class="preview-phone__camera" />
|
|
93
|
+
<iframe
|
|
94
|
+
ref="frameRef"
|
|
95
|
+
class="preview-phone__viewport"
|
|
96
|
+
:src="previewHref"
|
|
97
|
+
title="Mobile demo preview"
|
|
98
|
+
loading="lazy"
|
|
99
|
+
@load="syncTheme"
|
|
100
|
+
/>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<a class="preview-mobile-link" :href="previewHref" target="_blank" rel="noreferrer">
|
|
104
|
+
Open mobile preview
|
|
105
|
+
</a>
|
|
106
|
+
</section>
|
|
107
|
+
</template>
|
|
108
|
+
|
|
109
|
+
<style scoped lang="scss">
|
|
110
|
+
.preview-actions {
|
|
111
|
+
display: none;
|
|
112
|
+
align-items: center;
|
|
113
|
+
justify-content: space-between;
|
|
114
|
+
margin-bottom: 12px;
|
|
115
|
+
padding: 0 4px;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.preview-actions__label {
|
|
119
|
+
color: var(--vp-c-text-2);
|
|
120
|
+
font-size: 12px;
|
|
121
|
+
font-weight: 600;
|
|
122
|
+
letter-spacing: 0.08em;
|
|
123
|
+
text-transform: uppercase;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.preview-actions__link,
|
|
127
|
+
.preview-mobile-link {
|
|
128
|
+
color: var(--vp-c-brand-1);
|
|
129
|
+
font-size: 13px;
|
|
130
|
+
font-weight: 600;
|
|
131
|
+
text-decoration: none;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.preview-phone {
|
|
135
|
+
display: none;
|
|
136
|
+
position: relative;
|
|
137
|
+
width: var(--vp-mobile-preview-width);
|
|
138
|
+
padding: 14px 10px;
|
|
139
|
+
background: linear-gradient(180deg, rgba(255, 255, 255, 0.86), rgba(255, 255, 255, 0.64)), rgba(255, 255, 255, 0.7);
|
|
140
|
+
border: 1px solid rgba(15, 23, 42, 0.08);
|
|
141
|
+
border-radius: 32px;
|
|
142
|
+
box-shadow:
|
|
143
|
+
0 24px 80px rgba(15, 23, 42, 0.12),
|
|
144
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.7);
|
|
145
|
+
backdrop-filter: blur(20px);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
:global(.dark) .preview-phone {
|
|
149
|
+
background: linear-gradient(180deg, rgba(24, 24, 27, 0.92), rgba(24, 24, 27, 0.8)), rgba(24, 24, 27, 0.86);
|
|
150
|
+
border-color: rgba(255, 255, 255, 0.12);
|
|
151
|
+
box-shadow:
|
|
152
|
+
0 24px 80px rgba(0, 0, 0, 0.45),
|
|
153
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.preview-phone__camera {
|
|
157
|
+
position: absolute;
|
|
158
|
+
top: 8px;
|
|
159
|
+
left: 50%;
|
|
160
|
+
width: 104px;
|
|
161
|
+
height: 18px;
|
|
162
|
+
border-radius: 999px;
|
|
163
|
+
background: rgba(15, 23, 42, 0.92);
|
|
164
|
+
transform: translateX(-50%);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.preview-phone__viewport {
|
|
168
|
+
display: block;
|
|
169
|
+
width: 100%;
|
|
170
|
+
height: var(--vp-mobile-preview-height);
|
|
171
|
+
overflow: hidden;
|
|
172
|
+
background: var(--vp-c-bg);
|
|
173
|
+
border: 0;
|
|
174
|
+
border-radius: 24px;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.preview-mobile-link {
|
|
178
|
+
display: inline-flex;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
@media (min-width: 1440px) {
|
|
182
|
+
.preview-actions {
|
|
183
|
+
display: flex;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.preview-phone {
|
|
187
|
+
display: block;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.preview-mobile-link {
|
|
191
|
+
display: none;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
</style>
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Component } from 'vue'
|
|
3
|
+
import { useData } from 'vitepress'
|
|
4
|
+
import { computed, inject, markRaw, onBeforeUnmount, onMounted, ref, shallowRef } from 'vue'
|
|
5
|
+
import { mobilePreviewRegistryKey, resolveMobilePreviewId } from '../mobile-preview'
|
|
6
|
+
|
|
7
|
+
const { frontmatter, theme } = useData()
|
|
8
|
+
const registry = inject(mobilePreviewRegistryKey)
|
|
9
|
+
const demoId = ref('')
|
|
10
|
+
const errorMessage = ref('')
|
|
11
|
+
const isLoading = ref(true)
|
|
12
|
+
const demoComponent = shallowRef<Component>()
|
|
13
|
+
const previewConfig = computed(() => theme.value.mobilePreview ?? {})
|
|
14
|
+
|
|
15
|
+
const statusMessage = computed(() => {
|
|
16
|
+
if (errorMessage.value)
|
|
17
|
+
return errorMessage.value
|
|
18
|
+
|
|
19
|
+
return isLoading.value ? 'Loading mobile preview...' : 'No preview loaded.'
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
function applyTheme(theme: string | null): void {
|
|
23
|
+
if (typeof document === 'undefined')
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
document.documentElement.classList.toggle('dark', theme === 'dark')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function loadDemo(value: string | null): Promise<void> {
|
|
30
|
+
demoId.value = resolveMobilePreviewId(value ?? '', previewConfig.value.demoRoot)
|
|
31
|
+
demoComponent.value = undefined
|
|
32
|
+
errorMessage.value = ''
|
|
33
|
+
isLoading.value = true
|
|
34
|
+
|
|
35
|
+
if (!demoId.value) {
|
|
36
|
+
errorMessage.value = 'Missing demo id.'
|
|
37
|
+
isLoading.value = false
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!registry) {
|
|
42
|
+
errorMessage.value = 'No mobile preview registry was provided.'
|
|
43
|
+
isLoading.value = false
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const loader = registry[demoId.value]
|
|
48
|
+
if (!loader) {
|
|
49
|
+
errorMessage.value = `Unknown demo: ${demoId.value}`
|
|
50
|
+
isLoading.value = false
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const module = await loader()
|
|
56
|
+
const resolvedComponent = (module as { default?: Component }).default ?? module
|
|
57
|
+
demoComponent.value = markRaw(resolvedComponent as Component)
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
errorMessage.value = error instanceof Error
|
|
61
|
+
? error.message
|
|
62
|
+
: 'Failed to load the requested demo.'
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
isLoading.value = false
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function syncFromLocation(): void {
|
|
70
|
+
if (typeof window === 'undefined')
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
const search = new URLSearchParams(window.location.search)
|
|
74
|
+
const fallbackDemo = typeof frontmatter.value.mobileDemo === 'string'
|
|
75
|
+
? frontmatter.value.mobileDemo
|
|
76
|
+
: null
|
|
77
|
+
applyTheme(search.get('theme'))
|
|
78
|
+
void loadDemo(search.get('demo') ?? fallbackDemo)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function handleMessage(event: MessageEvent): void {
|
|
82
|
+
if (event.data?.type !== 'vp-mobile-preview-theme')
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
applyTheme(event.data.value)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
onMounted(() => {
|
|
89
|
+
syncFromLocation()
|
|
90
|
+
window.addEventListener('message', handleMessage)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
onBeforeUnmount(() => {
|
|
94
|
+
window.removeEventListener('message', handleMessage)
|
|
95
|
+
})
|
|
96
|
+
</script>
|
|
97
|
+
|
|
98
|
+
<template>
|
|
99
|
+
<main class="VPMobilePreviewLayout">
|
|
100
|
+
<component :is="demoComponent" v-if="demoComponent" />
|
|
101
|
+
<div v-else class="preview-status">
|
|
102
|
+
<p class="preview-status__title">
|
|
103
|
+
Mobile Preview
|
|
104
|
+
</p>
|
|
105
|
+
<p class="preview-status__body">
|
|
106
|
+
{{ statusMessage }}
|
|
107
|
+
</p>
|
|
108
|
+
</div>
|
|
109
|
+
</main>
|
|
110
|
+
</template>
|
|
111
|
+
|
|
112
|
+
<style scoped lang="scss">
|
|
113
|
+
.VPMobilePreviewLayout {
|
|
114
|
+
min-height: 100%;
|
|
115
|
+
height: 100vh;
|
|
116
|
+
overflow-x: hidden;
|
|
117
|
+
overflow-y: auto;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.preview-status {
|
|
121
|
+
display: grid;
|
|
122
|
+
place-items: center;
|
|
123
|
+
align-content: center;
|
|
124
|
+
min-height: 100vh;
|
|
125
|
+
padding: 24px;
|
|
126
|
+
color: var(--vp-c-text-2);
|
|
127
|
+
text-align: center;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.preview-status__title {
|
|
131
|
+
margin: 0 0 12px;
|
|
132
|
+
color: var(--vp-c-text-1);
|
|
133
|
+
font-weight: 600;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.preview-status__body {
|
|
137
|
+
margin: 0;
|
|
138
|
+
font-size: 14px;
|
|
139
|
+
}
|
|
140
|
+
</style>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { InjectionKey } from 'vue'
|
|
2
|
+
|
|
3
|
+
export type MobilePreviewModule = () => Promise<unknown>
|
|
4
|
+
|
|
5
|
+
export type MobilePreviewRegistry = Record<string, MobilePreviewModule>
|
|
6
|
+
|
|
7
|
+
export interface EPMobilePreviewConfig {
|
|
8
|
+
/**
|
|
9
|
+
* Locale-relative path of the standalone preview page.
|
|
10
|
+
*
|
|
11
|
+
* @default '/preview/'
|
|
12
|
+
*/
|
|
13
|
+
previewPath?: string
|
|
14
|
+
/**
|
|
15
|
+
* Outer device frame width used in the doc layout.
|
|
16
|
+
*
|
|
17
|
+
* @default 390
|
|
18
|
+
*/
|
|
19
|
+
deviceWidth?: number
|
|
20
|
+
/**
|
|
21
|
+
* Inner viewport height used in the doc layout.
|
|
22
|
+
*
|
|
23
|
+
* @default 760
|
|
24
|
+
*/
|
|
25
|
+
deviceHeight?: number
|
|
26
|
+
/**
|
|
27
|
+
* Root directory used to resolve the `mobileDemo` frontmatter field.
|
|
28
|
+
*
|
|
29
|
+
* @default 'demo/'
|
|
30
|
+
*/
|
|
31
|
+
demoRoot?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const mobilePreviewRegistryKey: InjectionKey<MobilePreviewRegistry> = Symbol('vitepress-theme-element-plus.mobile-preview-registry')
|
|
35
|
+
|
|
36
|
+
export function normalizeMobilePreviewId(value: string): string {
|
|
37
|
+
return value
|
|
38
|
+
.trim()
|
|
39
|
+
.replace(/\\/g, '/')
|
|
40
|
+
.replace(/^\.?\//, '')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function normalizeMobilePreviewRoot(value: string | undefined): string {
|
|
44
|
+
if (!value?.trim())
|
|
45
|
+
return 'demo/'
|
|
46
|
+
|
|
47
|
+
return normalizeMobilePreviewId(value)
|
|
48
|
+
.replace(/\/?$/, '/')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function resolveMobilePreviewId(value: string | undefined, root: string | undefined): string {
|
|
52
|
+
const normalizedId = normalizeMobilePreviewId(value ?? '')
|
|
53
|
+
if (!normalizedId)
|
|
54
|
+
return ''
|
|
55
|
+
|
|
56
|
+
const normalizedRoot = normalizeMobilePreviewRoot(root)
|
|
57
|
+
return normalizedId.startsWith(normalizedRoot)
|
|
58
|
+
? normalizedId
|
|
59
|
+
: `${normalizedRoot}${normalizedId}`
|
|
60
|
+
}
|
package/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DefaultTheme, Theme } from 'vitepress'
|
|
2
|
+
import type { EPMobilePreviewConfig, MobilePreviewModule, MobilePreviewRegistry } from './client/mobile-preview'
|
|
2
3
|
import VPTheme from 'vitepress/theme'
|
|
3
4
|
import Layout from './client/components/Layout.vue'
|
|
4
5
|
import 'element-plus/theme-chalk/base.css'
|
|
@@ -37,9 +38,20 @@ export interface EPThemeConfig extends DefaultTheme.Config {
|
|
|
37
38
|
* 文档版本号
|
|
38
39
|
*/
|
|
39
40
|
version?: string
|
|
41
|
+
/**
|
|
42
|
+
* 移动端预览配置
|
|
43
|
+
*/
|
|
44
|
+
mobilePreview?: EPMobilePreviewConfig
|
|
40
45
|
footer?: EPThemeFooter
|
|
41
46
|
}
|
|
42
47
|
// #endregion snippet
|
|
43
48
|
|
|
44
49
|
export { Layout }
|
|
50
|
+
export {
|
|
51
|
+
mobilePreviewRegistryKey,
|
|
52
|
+
normalizeMobilePreviewId,
|
|
53
|
+
normalizeMobilePreviewRoot,
|
|
54
|
+
resolveMobilePreviewId,
|
|
55
|
+
} from './client/mobile-preview'
|
|
56
|
+
export type { EPMobilePreviewConfig, MobilePreviewModule, MobilePreviewRegistry }
|
|
45
57
|
export default EPTheme
|