mirror of https://github.com/vuejs/vitepress
parent
9fc8462726
commit
bfc30c4de3
@ -0,0 +1,96 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, provide, useSlots } from 'vue'
|
||||
import VPBackdrop from './components/VPBackdrop.vue'
|
||||
import VPContent from './components/VPContent.vue'
|
||||
import VPFooter from './components/VPFooter.vue'
|
||||
import VPLocalNav from './components/VPLocalNav.vue'
|
||||
import VPNav from './components/VPNav.vue'
|
||||
import VPSidebar from './components/VPSidebar.vue'
|
||||
import VPSkipLink from './components/VPSkipLink.vue'
|
||||
import { useData } from './composables/data'
|
||||
import { layoutInfoInjectionKey, registerWatchers } from './composables/layout'
|
||||
import { useSidebarControl } from './composables/sidebar'
|
||||
|
||||
const {
|
||||
isOpen: isSidebarOpen,
|
||||
open: openSidebar,
|
||||
close: closeSidebar
|
||||
} = useSidebarControl()
|
||||
|
||||
registerWatchers({ closeSidebar })
|
||||
|
||||
const { frontmatter } = useData()
|
||||
|
||||
const slots = useSlots()
|
||||
const heroImageSlotExists = computed(() => !!slots['home-hero-image'])
|
||||
|
||||
// TODO: Check if we still need this.
|
||||
provide(layoutInfoInjectionKey, { heroImageSlotExists })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="frontmatter.layout !== false"
|
||||
class="vp-layout"
|
||||
:class="frontmatter.pageClass"
|
||||
>
|
||||
<slot name="layout-top" />
|
||||
<VPSkipLink />
|
||||
<VPBackdrop class="backdrop" :show="isSidebarOpen" @click="closeSidebar" />
|
||||
<VPNav>
|
||||
<template #nav-bar-title-before><slot name="nav-bar-title-before" /></template>
|
||||
<template #nav-bar-title-after><slot name="nav-bar-title-after" /></template>
|
||||
<template #nav-bar-content-before><slot name="nav-bar-content-before" /></template>
|
||||
<template #nav-bar-content-after><slot name="nav-bar-content-after" /></template>
|
||||
<template #nav-screen-content-before><slot name="nav-screen-content-before" /></template>
|
||||
<template #nav-screen-content-after><slot name="nav-screen-content-after" /></template>
|
||||
</VPNav>
|
||||
<!-- <VPLocalNav :open="isSidebarOpen" @open-menu="openSidebar" /> -->
|
||||
|
||||
<!-- <VPSidebar :open="isSidebarOpen">
|
||||
<template #sidebar-nav-before><slot name="sidebar-nav-before" /></template>
|
||||
<template #sidebar-nav-after><slot name="sidebar-nav-after" /></template>
|
||||
</VPSidebar> -->
|
||||
|
||||
<VPContent>
|
||||
<template #page-top><slot name="page-top" /></template>
|
||||
<template #page-bottom><slot name="page-bottom" /></template>
|
||||
|
||||
<template #not-found><slot name="not-found" /></template>
|
||||
<template #home-hero-before><slot name="home-hero-before" /></template>
|
||||
<template #home-hero-info-before><slot name="home-hero-info-before" /></template>
|
||||
<template #home-hero-info><slot name="home-hero-info" /></template>
|
||||
<template #home-hero-info-after><slot name="home-hero-info-after" /></template>
|
||||
<template #home-hero-actions-after><slot name="home-hero-actions-after" /></template>
|
||||
<template #home-hero-image><slot name="home-hero-image" /></template>
|
||||
<template #home-hero-after><slot name="home-hero-after" /></template>
|
||||
<template #home-features-before><slot name="home-features-before" /></template>
|
||||
<template #home-features-after><slot name="home-features-after" /></template>
|
||||
|
||||
<template #doc-footer-before><slot name="doc-footer-before" /></template>
|
||||
<template #doc-before><slot name="doc-before" /></template>
|
||||
<template #doc-after><slot name="doc-after" /></template>
|
||||
<template #doc-top><slot name="doc-top" /></template>
|
||||
<template #doc-bottom><slot name="doc-bottom" /></template>
|
||||
|
||||
<template #aside-top><slot name="aside-top" /></template>
|
||||
<template #aside-bottom><slot name="aside-bottom" /></template>
|
||||
<template #aside-outline-before><slot name="aside-outline-before" /></template>
|
||||
<template #aside-outline-after><slot name="aside-outline-after" /></template>
|
||||
<template #aside-ads-before><slot name="aside-ads-before" /></template>
|
||||
<template #aside-ads-after><slot name="aside-ads-after" /></template>
|
||||
</VPContent>
|
||||
|
||||
<!-- <VPFooter /> -->
|
||||
<slot name="layout-bottom" />
|
||||
</div>
|
||||
<Content v-else />
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.vp-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,96 @@
|
||||
<script setup lang="ts">
|
||||
import { withBase } from 'vitepress'
|
||||
import { useData } from './composables/data'
|
||||
import { useLangs } from './composables/langs'
|
||||
|
||||
const { theme } = useData()
|
||||
const { currentLang } = useLangs()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="NotFound">
|
||||
<p class="code">{{ theme.notFound?.code ?? '404' }}</p>
|
||||
<h1 class="title">{{ theme.notFound?.title ?? 'PAGE NOT FOUND' }}</h1>
|
||||
<div class="divider" />
|
||||
<blockquote class="quote">
|
||||
{{
|
||||
theme.notFound?.quote ??
|
||||
"But if you don't change your direction, and if you keep looking, you may end up where you are heading."
|
||||
}}
|
||||
</blockquote>
|
||||
|
||||
<div class="action">
|
||||
<a
|
||||
class="link"
|
||||
:href="withBase(theme.notFound?.link ?? currentLang.link)"
|
||||
:aria-label="theme.notFound?.linkLabel ?? 'go to home'"
|
||||
>
|
||||
{{ theme.notFound?.linkText ?? 'Take me home' }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.NotFound {
|
||||
padding: 64px 24px 96px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.NotFound {
|
||||
padding: 96px 32px 168px;
|
||||
}
|
||||
}
|
||||
|
||||
.code {
|
||||
line-height: 64px;
|
||||
font-size: 64px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.title {
|
||||
padding-top: 12px;
|
||||
letter-spacing: 2px;
|
||||
line-height: 20px;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin: 24px auto 18px;
|
||||
width: 64px;
|
||||
height: 1px;
|
||||
background-color: var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.quote {
|
||||
margin: 0 auto;
|
||||
max-width: 256px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.action {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.link {
|
||||
display: inline-block;
|
||||
border: 1px solid var(--vp-c-brand-1);
|
||||
border-radius: 16px;
|
||||
padding: 3px 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-brand-1);
|
||||
transition:
|
||||
border-color 0.25s,
|
||||
color 0.25s;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
border-color: var(--vp-c-brand-2);
|
||||
color: var(--vp-c-brand-2);
|
||||
}
|
||||
</style>
|
@ -0,0 +1,92 @@
|
||||
<script setup lang="ts">
|
||||
import docsearch from '@docsearch/js'
|
||||
import { useRouter } from 'vitepress'
|
||||
import type { DefaultTheme } from 'vitepress/theme'
|
||||
import { nextTick, onMounted, watch } from 'vue'
|
||||
import { useData } from '../composables/data'
|
||||
|
||||
const props = defineProps<{
|
||||
algolia: DefaultTheme.AlgoliaSearchOptions
|
||||
}>()
|
||||
|
||||
const router = useRouter()
|
||||
const { site, localeIndex, lang } = useData()
|
||||
|
||||
onMounted(update)
|
||||
watch(localeIndex, update)
|
||||
|
||||
async function update() {
|
||||
await nextTick()
|
||||
const options = {
|
||||
...props.algolia,
|
||||
...props.algolia.locales?.[localeIndex.value]
|
||||
}
|
||||
const rawFacetFilters = options.searchParameters?.facetFilters ?? []
|
||||
const facetFilters = [
|
||||
...(Array.isArray(rawFacetFilters)
|
||||
? rawFacetFilters
|
||||
: [rawFacetFilters]
|
||||
).filter((f) => !f.startsWith('lang:')),
|
||||
`lang:${lang.value}`
|
||||
]
|
||||
|
||||
// Rebuild the askAi prop as an object:
|
||||
// If the askAi prop is a string, treat it as the assistantId and use
|
||||
// the default indexName, apiKey and appId from the main options.
|
||||
// If the askAi prop is an object, spread its explicit values.
|
||||
const askAiProp = options.askAi
|
||||
const isAskAiString = typeof askAiProp === 'string'
|
||||
|
||||
const askAi = askAiProp
|
||||
? {
|
||||
indexName: isAskAiString ? options.indexName : askAiProp.indexName,
|
||||
apiKey: isAskAiString ? options.apiKey : askAiProp.apiKey,
|
||||
appId: isAskAiString ? options.appId : askAiProp.appId,
|
||||
assistantId: isAskAiString ? askAiProp : askAiProp.assistantId,
|
||||
// Re-use the merged facetFilters from the search parameters so that
|
||||
// Ask AI uses the same language filtering as the regular search.
|
||||
searchParameters: facetFilters.length ? { facetFilters } : undefined
|
||||
}
|
||||
: undefined
|
||||
|
||||
initialize({
|
||||
...options,
|
||||
searchParameters: {
|
||||
...options.searchParameters,
|
||||
facetFilters
|
||||
},
|
||||
askAi
|
||||
})
|
||||
}
|
||||
|
||||
function initialize(userOptions: DefaultTheme.AlgoliaSearchOptions) {
|
||||
const options = Object.assign({}, userOptions, {
|
||||
container: '#docsearch',
|
||||
|
||||
navigator: {
|
||||
navigate(item: { itemUrl: string }) {
|
||||
router.go(item.itemUrl)
|
||||
}
|
||||
},
|
||||
|
||||
transformItems(items: { url: string }[]) {
|
||||
return items.map((item) => {
|
||||
return Object.assign({}, item, {
|
||||
url: getRelativePath(item.url)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
docsearch(options as any)
|
||||
}
|
||||
|
||||
function getRelativePath(url: string) {
|
||||
const { pathname, hash } = new URL(url, location.origin)
|
||||
return pathname.replace(/\.html$/, site.value.cleanUrls ? '' : '.html') + hash
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="docsearch" />
|
||||
</template>
|
@ -0,0 +1,41 @@
|
||||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
show: boolean
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<transition name="fade">
|
||||
<div v-if="show" class="VPBackdrop" />
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPBackdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
/*rtl:ignore*/
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
/*rtl:ignore*/
|
||||
left: 0;
|
||||
z-index: var(--vp-z-index-backdrop);
|
||||
background: var(--vp-backdrop-bg-color);
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
|
||||
.VPBackdrop.fade-enter-from,
|
||||
.VPBackdrop.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.VPBackdrop.fade-leave-active {
|
||||
transition-duration: .25s;
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.VPBackdrop {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,86 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
text?: string
|
||||
type?: 'info' | 'tip' | 'warning' | 'danger'
|
||||
}
|
||||
withDefaults(defineProps<Props>(), {
|
||||
type: 'tip'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="VPBadge" :class="type">
|
||||
<slot>{{ text }}</slot>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.VPBadge {
|
||||
display: inline-block;
|
||||
margin-left: 2px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 12px;
|
||||
padding: 0 10px;
|
||||
line-height: 22px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.VPBadge.small {
|
||||
padding: 0 6px;
|
||||
line-height: 18px;
|
||||
font-size: 10px;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
|
||||
.VPDocFooter .VPBadge {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.vp-doc h1 > .VPBadge {
|
||||
margin-top: 4px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.vp-doc h2 > .VPBadge {
|
||||
margin-top: 3px;
|
||||
padding: 0 8px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.vp-doc h3 > .VPBadge {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.vp-doc h4 > .VPBadge,
|
||||
.vp-doc h5 > .VPBadge,
|
||||
.vp-doc h6 > .VPBadge {
|
||||
vertical-align: middle;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.VPBadge.info {
|
||||
border-color: var(--vp-badge-info-border);
|
||||
color: var(--vp-badge-info-text);
|
||||
background-color: var(--vp-badge-info-bg);
|
||||
}
|
||||
|
||||
.VPBadge.tip {
|
||||
border-color: var(--vp-badge-tip-border);
|
||||
color: var(--vp-badge-tip-text);
|
||||
background-color: var(--vp-badge-tip-bg);
|
||||
}
|
||||
|
||||
.VPBadge.warning {
|
||||
border-color: var(--vp-badge-warning-border);
|
||||
color: var(--vp-badge-warning-text);
|
||||
background-color: var(--vp-badge-warning-bg);
|
||||
}
|
||||
|
||||
.VPBadge.danger {
|
||||
border-color: var(--vp-badge-danger-border);
|
||||
color: var(--vp-badge-danger-text);
|
||||
background-color: var(--vp-badge-danger-bg);
|
||||
}
|
||||
</style>
|
@ -0,0 +1,123 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { normalizeLink } from '../support/utils'
|
||||
import { EXTERNAL_URL_RE } from '../../shared'
|
||||
|
||||
interface Props {
|
||||
tag?: string
|
||||
size?: 'medium' | 'big'
|
||||
theme?: 'brand' | 'alt' | 'sponsor'
|
||||
text?: string
|
||||
href?: string
|
||||
target?: string;
|
||||
rel?: string;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
size: 'medium',
|
||||
theme: 'brand'
|
||||
})
|
||||
|
||||
const isExternal = computed(
|
||||
() => props.href && EXTERNAL_URL_RE.test(props.href)
|
||||
)
|
||||
|
||||
const component = computed(() => {
|
||||
return props.tag || (props.href ? 'a' : 'button')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="component"
|
||||
class="VPButton"
|
||||
:class="[size, theme]"
|
||||
:href="href ? normalizeLink(href) : undefined"
|
||||
:target="props.target ?? (isExternal ? '_blank' : undefined)"
|
||||
:rel="props.rel ?? (isExternal ? 'noreferrer' : undefined)"
|
||||
>
|
||||
<slot>{{ text }}</slot>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPButton {
|
||||
display: inline-block;
|
||||
border: 1px solid transparent;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
transition: color 0.25s, border-color 0.25s, background-color 0.25s;
|
||||
}
|
||||
|
||||
.VPButton:active {
|
||||
transition: color 0.1s, border-color 0.1s, background-color 0.1s;
|
||||
}
|
||||
|
||||
.VPButton.medium {
|
||||
border-radius: 20px;
|
||||
padding: 0 20px;
|
||||
line-height: 38px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.VPButton.big {
|
||||
border-radius: 24px;
|
||||
padding: 0 24px;
|
||||
line-height: 46px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.VPButton.brand {
|
||||
border-color: var(--vp-button-brand-border);
|
||||
color: var(--vp-button-brand-text);
|
||||
background-color: var(--vp-button-brand-bg);
|
||||
}
|
||||
|
||||
.VPButton.brand:hover {
|
||||
border-color: var(--vp-button-brand-hover-border);
|
||||
color: var(--vp-button-brand-hover-text);
|
||||
background-color: var(--vp-button-brand-hover-bg);
|
||||
}
|
||||
|
||||
.VPButton.brand:active {
|
||||
border-color: var(--vp-button-brand-active-border);
|
||||
color: var(--vp-button-brand-active-text);
|
||||
background-color: var(--vp-button-brand-active-bg);
|
||||
}
|
||||
|
||||
.VPButton.alt {
|
||||
border-color: var(--vp-button-alt-border);
|
||||
color: var(--vp-button-alt-text);
|
||||
background-color: var(--vp-button-alt-bg);
|
||||
}
|
||||
|
||||
.VPButton.alt:hover {
|
||||
border-color: var(--vp-button-alt-hover-border);
|
||||
color: var(--vp-button-alt-hover-text);
|
||||
background-color: var(--vp-button-alt-hover-bg);
|
||||
}
|
||||
|
||||
.VPButton.alt:active {
|
||||
border-color: var(--vp-button-alt-active-border);
|
||||
color: var(--vp-button-alt-active-text);
|
||||
background-color: var(--vp-button-alt-active-bg);
|
||||
}
|
||||
|
||||
.VPButton.sponsor {
|
||||
border-color: var(--vp-button-sponsor-border);
|
||||
color: var(--vp-button-sponsor-text);
|
||||
background-color: var(--vp-button-sponsor-bg);
|
||||
}
|
||||
|
||||
.VPButton.sponsor:hover {
|
||||
border-color: var(--vp-button-sponsor-hover-border);
|
||||
color: var(--vp-button-sponsor-hover-text);
|
||||
background-color: var(--vp-button-sponsor-hover-bg);
|
||||
}
|
||||
|
||||
.VPButton.sponsor:active {
|
||||
border-color: var(--vp-button-sponsor-active-border);
|
||||
color: var(--vp-button-sponsor-active-text);
|
||||
background-color: var(--vp-button-sponsor-active-bg);
|
||||
}
|
||||
</style>
|
@ -0,0 +1,109 @@
|
||||
<script setup lang="ts">
|
||||
import type { DefaultTheme } from 'vitepress/theme'
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import { useAside } from '../composables/aside'
|
||||
import { useData } from '../composables/data'
|
||||
|
||||
const { page } = useData()
|
||||
const props = defineProps<{
|
||||
carbonAds: DefaultTheme.CarbonAdsOptions
|
||||
}>()
|
||||
|
||||
const carbonOptions = props.carbonAds
|
||||
|
||||
const { isAsideEnabled } = useAside()
|
||||
const container = ref()
|
||||
|
||||
let isInitialized = false
|
||||
|
||||
function init() {
|
||||
if (!isInitialized) {
|
||||
isInitialized = true
|
||||
const s = document.createElement('script')
|
||||
s.id = '_carbonads_js'
|
||||
s.src = `//cdn.carbonads.com/carbon.js?serve=${carbonOptions.code}&placement=${carbonOptions.placement}`
|
||||
s.async = true
|
||||
container.value.appendChild(s)
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => page.value.relativePath, () => {
|
||||
if (isInitialized && isAsideEnabled.value) {
|
||||
;(window as any)._carbonads?.refresh()
|
||||
}
|
||||
})
|
||||
|
||||
// no need to account for option changes during dev, we can just
|
||||
// refresh the page
|
||||
if (carbonOptions) {
|
||||
onMounted(() => {
|
||||
// if the page is loaded when aside is active, load carbon directly.
|
||||
// otherwise, only load it if the page resizes to wide enough. this avoids
|
||||
// loading carbon at all on mobile where it's never shown
|
||||
if (isAsideEnabled.value) {
|
||||
init()
|
||||
} else {
|
||||
watch(isAsideEnabled, (wide) => wide && init())
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="VPCarbonAds" ref="container" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPCarbonAds {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 24px;
|
||||
border-radius: 12px;
|
||||
min-height: 256px;
|
||||
text-align: center;
|
||||
line-height: 18px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
background-color: var(--vp-carbon-ads-bg-color);
|
||||
}
|
||||
|
||||
.VPCarbonAds :deep(img) {
|
||||
margin: 0 auto;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.VPCarbonAds :deep(.carbon-text) {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
padding-top: 12px;
|
||||
color: var(--vp-carbon-ads-text-color);
|
||||
transition: color 0.25s;
|
||||
}
|
||||
|
||||
.VPCarbonAds :deep(.carbon-text:hover) {
|
||||
color: var(--vp-carbon-ads-hover-text-color);
|
||||
}
|
||||
|
||||
.VPCarbonAds :deep(.carbon-poweredby) {
|
||||
display: block;
|
||||
padding-top: 6px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-carbon-ads-poweredby-color);
|
||||
text-transform: uppercase;
|
||||
transition: color 0.25s;
|
||||
}
|
||||
|
||||
.VPCarbonAds :deep(.carbon-poweredby:hover) {
|
||||
color: var(--vp-carbon-ads-hover-poweredby-color);
|
||||
}
|
||||
|
||||
.VPCarbonAds :deep(> div) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.VPCarbonAds :deep(> div:first-of-type) {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,95 @@
|
||||
<script setup lang="ts">
|
||||
import NotFound from '../NotFound.vue'
|
||||
import { useData } from '../composables/data'
|
||||
import { useLayout } from '../composables/layout'
|
||||
import VPDoc from './VPDoc.vue'
|
||||
import VPHome from './VPHome.vue'
|
||||
import VPPage from './VPPage.vue'
|
||||
|
||||
const { page, frontmatter } = useData()
|
||||
const { isHome, hasSidebar } = useLayout()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="VPContent"
|
||||
id="VPContent"
|
||||
:class="{ 'has-sidebar': hasSidebar, 'is-home': isHome }"
|
||||
>
|
||||
<slot name="not-found" v-if="page.isNotFound"><NotFound /></slot>
|
||||
|
||||
<VPPage v-else-if="frontmatter.layout === 'page'">
|
||||
<template #page-top><slot name="page-top" /></template>
|
||||
<template #page-bottom><slot name="page-bottom" /></template>
|
||||
</VPPage>
|
||||
|
||||
<VPHome v-else-if="frontmatter.layout === 'home'">
|
||||
<template #home-hero-before><slot name="home-hero-before" /></template>
|
||||
<template #home-hero-info-before><slot name="home-hero-info-before" /></template>
|
||||
<template #home-hero-info><slot name="home-hero-info" /></template>
|
||||
<template #home-hero-info-after><slot name="home-hero-info-after" /></template>
|
||||
<template #home-hero-actions-after><slot name="home-hero-actions-after" /></template>
|
||||
<template #home-hero-image><slot name="home-hero-image" /></template>
|
||||
<template #home-hero-after><slot name="home-hero-after" /></template>
|
||||
<template #home-features-before><slot name="home-features-before" /></template>
|
||||
<template #home-features-after><slot name="home-features-after" /></template>
|
||||
</VPHome>
|
||||
|
||||
<component
|
||||
v-else-if="frontmatter.layout && frontmatter.layout !== 'doc'"
|
||||
:is="frontmatter.layout"
|
||||
/>
|
||||
|
||||
<VPDoc v-else>
|
||||
<template #doc-top><slot name="doc-top" /></template>
|
||||
<template #doc-bottom><slot name="doc-bottom" /></template>
|
||||
|
||||
<template #doc-footer-before><slot name="doc-footer-before" /></template>
|
||||
<template #doc-before><slot name="doc-before" /></template>
|
||||
<template #doc-after><slot name="doc-after" /></template>
|
||||
|
||||
<template #aside-top><slot name="aside-top" /></template>
|
||||
<template #aside-outline-before><slot name="aside-outline-before" /></template>
|
||||
<template #aside-outline-after><slot name="aside-outline-after" /></template>
|
||||
<template #aside-ads-before><slot name="aside-ads-before" /></template>
|
||||
<template #aside-ads-after><slot name="aside-ads-after" /></template>
|
||||
<template #aside-bottom><slot name="aside-bottom" /></template>
|
||||
</VPDoc>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPContent {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 0;
|
||||
margin: var(--vp-layout-top-height, 0px) auto 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.VPContent.is-home {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.VPContent.has-sidebar {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.VPContent {
|
||||
padding-top: var(--vp-nav-height);
|
||||
}
|
||||
|
||||
.VPContent.has-sidebar {
|
||||
margin: var(--vp-layout-top-height, 0px) 0 0;
|
||||
padding-left: var(--vp-sidebar-width);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1440px) {
|
||||
.VPContent.has-sidebar {
|
||||
padding-right: calc((100vw - var(--vp-layout-max-width)) / 2);
|
||||
padding-left: calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width));
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,194 @@
|
||||
<script setup lang="ts">
|
||||
import { useRoute } from 'vitepress'
|
||||
import { computed } from 'vue'
|
||||
import { useData } from '../composables/data'
|
||||
import { useLayout } from '../composables/layout'
|
||||
import VPDocAside from './VPDocAside.vue'
|
||||
import VPDocFooter from './VPDocFooter.vue'
|
||||
|
||||
const { theme } = useData()
|
||||
|
||||
const route = useRoute()
|
||||
const { hasSidebar, hasAside, leftAside } = useLayout()
|
||||
|
||||
const pageName = computed(() =>
|
||||
route.path.replace(/[./]+/g, '_').replace(/_html$/, '')
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="VPDoc"
|
||||
:class="{ 'has-sidebar': hasSidebar, 'has-aside': hasAside }"
|
||||
>
|
||||
<slot name="doc-top" />
|
||||
<div class="container">
|
||||
<div v-if="hasAside" class="aside" :class="{'left-aside': leftAside}">
|
||||
<div class="aside-curtain" />
|
||||
<div class="aside-container">
|
||||
<div class="aside-content">
|
||||
<VPDocAside>
|
||||
<template #aside-top><slot name="aside-top" /></template>
|
||||
<template #aside-bottom><slot name="aside-bottom" /></template>
|
||||
<template #aside-outline-before><slot name="aside-outline-before" /></template>
|
||||
<template #aside-outline-after><slot name="aside-outline-after" /></template>
|
||||
<template #aside-ads-before><slot name="aside-ads-before" /></template>
|
||||
<template #aside-ads-after><slot name="aside-ads-after" /></template>
|
||||
</VPDocAside>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="content-container">
|
||||
<slot name="doc-before" />
|
||||
<main class="main">
|
||||
<Content
|
||||
class="vp-doc"
|
||||
:class="[
|
||||
pageName,
|
||||
theme.externalLinkIcon && 'external-link-icon-enabled'
|
||||
]"
|
||||
/>
|
||||
</main>
|
||||
<VPDocFooter>
|
||||
<template #doc-footer-before><slot name="doc-footer-before" /></template>
|
||||
</VPDocFooter>
|
||||
<slot name="doc-after" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<slot name="doc-bottom" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPDoc {
|
||||
padding: 32px 24px 96px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.VPDoc {
|
||||
padding: 48px 32px 128px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.VPDoc {
|
||||
padding: 48px 32px 0;
|
||||
}
|
||||
|
||||
.VPDoc:not(.has-sidebar) .container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
max-width: 992px;
|
||||
}
|
||||
|
||||
.VPDoc:not(.has-sidebar) .content {
|
||||
max-width: 752px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.VPDoc .container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.VPDoc .aside {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1440px) {
|
||||
.VPDoc:not(.has-sidebar) .content {
|
||||
max-width: 784px;
|
||||
}
|
||||
|
||||
.VPDoc:not(.has-sidebar) .container {
|
||||
max-width: 1104px;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.aside {
|
||||
position: relative;
|
||||
display: none;
|
||||
order: 2;
|
||||
flex-grow: 1;
|
||||
padding-left: 32px;
|
||||
width: 100%;
|
||||
max-width: 256px;
|
||||
}
|
||||
|
||||
.left-aside {
|
||||
order: 1;
|
||||
padding-left: unset;
|
||||
padding-right: 32px;
|
||||
}
|
||||
|
||||
.aside-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
padding-top: calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + var(--vp-doc-top-height, 0px) + 48px);
|
||||
width: 224px;
|
||||
height: 100vh;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.aside-container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.aside-curtain {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
z-index: 10;
|
||||
width: 224px;
|
||||
height: 32px;
|
||||
background: linear-gradient(transparent, var(--vp-c-bg) 70%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.aside-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: calc(100vh - (var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 48px));
|
||||
padding-bottom: 32px;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.content {
|
||||
padding: 0 32px 128px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.content {
|
||||
order: 1;
|
||||
margin: 0;
|
||||
min-width: 640px;
|
||||
}
|
||||
}
|
||||
|
||||
.content-container {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.VPDoc.has-aside .content-container {
|
||||
max-width: 688px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
import { useData } from '../composables/data'
|
||||
import VPDocAsideOutline from './VPDocAsideOutline.vue'
|
||||
import VPDocAsideCarbonAds from './VPDocAsideCarbonAds.vue'
|
||||
|
||||
const { theme } = useData()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="VPDocAside">
|
||||
<slot name="aside-top" />
|
||||
|
||||
<slot name="aside-outline-before" />
|
||||
<VPDocAsideOutline />
|
||||
<slot name="aside-outline-after" />
|
||||
|
||||
<div class="spacer" />
|
||||
|
||||
<slot name="aside-ads-before" />
|
||||
<VPDocAsideCarbonAds v-if="theme.carbonAds" :carbon-ads="theme.carbonAds" />
|
||||
<slot name="aside-ads-after" />
|
||||
|
||||
<slot name="aside-bottom" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPDocAside {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.VPDocAside :deep(.spacer + .VPDocAsideSponsors),
|
||||
.VPDocAside :deep(.spacer + .VPDocAsideCarbonAds) {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.VPDocAside :deep(.VPDocAsideSponsors + .VPDocAsideCarbonAds) {
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
import type { DefaultTheme } from 'vitepress/theme'
|
||||
|
||||
defineProps<{
|
||||
carbonAds: DefaultTheme.CarbonAdsOptions
|
||||
}>()
|
||||
|
||||
const VPCarbonAds = __CARBON__
|
||||
? defineAsyncComponent(() => import('./VPCarbonAds.vue'))
|
||||
: () => null
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="VPDocAsideCarbonAds">
|
||||
<VPCarbonAds :carbon-ads="carbonAds" />
|
||||
</div>
|
||||
</template>
|
@ -0,0 +1,80 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useData } from '../composables/data'
|
||||
import { resolveTitle, useActiveAnchor } from '../composables/outline'
|
||||
import VPDocOutlineItem from './VPDocOutlineItem.vue'
|
||||
import { useLayout } from '../composables/layout'
|
||||
|
||||
const { theme } = useData()
|
||||
|
||||
const container = ref()
|
||||
const marker = ref()
|
||||
|
||||
const { headers, hasLocalNav } = useLayout()
|
||||
|
||||
useActiveAnchor(container, marker)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav
|
||||
aria-labelledby="doc-outline-aria-label"
|
||||
class="VPDocAsideOutline"
|
||||
:class="{ 'has-outline': hasLocalNav }"
|
||||
ref="container"
|
||||
>
|
||||
<div class="content">
|
||||
<div class="outline-marker" ref="marker" />
|
||||
|
||||
<div
|
||||
aria-level="2"
|
||||
class="outline-title"
|
||||
id="doc-outline-aria-label"
|
||||
role="heading"
|
||||
>
|
||||
{{ resolveTitle(theme) }}
|
||||
</div>
|
||||
|
||||
<VPDocOutlineItem :headers :root="true" />
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPDocAsideOutline {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.VPDocAsideOutline.has-outline {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
border-left: 1px solid var(--vp-c-divider);
|
||||
padding-left: 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.outline-marker {
|
||||
position: absolute;
|
||||
top: 32px;
|
||||
left: -1px;
|
||||
z-index: 0;
|
||||
opacity: 0;
|
||||
width: 2px;
|
||||
border-radius: 2px;
|
||||
height: 18px;
|
||||
background-color: var(--vp-c-brand-1);
|
||||
transition:
|
||||
top 0.25s cubic-bezier(0, 1, 0.5, 1),
|
||||
background-color 0.5s,
|
||||
opacity 0.25s;
|
||||
}
|
||||
|
||||
.outline-title {
|
||||
line-height: 32px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { Sponsors } from './VPSponsors.vue'
|
||||
import type { Sponsor } from './VPSponsorsGrid.vue'
|
||||
import VPSponsors from './VPSponsors.vue'
|
||||
|
||||
defineProps<{
|
||||
tier?: string
|
||||
size?: 'xmini' | 'mini' | 'small'
|
||||
data: Sponsors[] | Sponsor[]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="VPDocAsideSponsors">
|
||||
<VPSponsors mode="aside" :tier :size :data />
|
||||
</div>
|
||||
</template>
|
@ -0,0 +1,167 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useData } from '../composables/data'
|
||||
import { useEditLink } from '../composables/edit-link'
|
||||
import { usePrevNext } from '../composables/prev-next'
|
||||
import VPLink from './VPLink.vue'
|
||||
import VPDocFooterLastUpdated from './VPDocFooterLastUpdated.vue'
|
||||
|
||||
const { theme, page, frontmatter } = useData()
|
||||
|
||||
const editLink = useEditLink()
|
||||
const control = usePrevNext()
|
||||
|
||||
const hasEditLink = computed(
|
||||
() => theme.value.editLink && frontmatter.value.editLink !== false
|
||||
)
|
||||
const hasLastUpdated = computed(() => page.value.lastUpdated)
|
||||
const showFooter = computed(
|
||||
() =>
|
||||
hasEditLink.value ||
|
||||
hasLastUpdated.value ||
|
||||
control.value.prev ||
|
||||
control.value.next
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer v-if="showFooter" class="VPDocFooter">
|
||||
<slot name="doc-footer-before" />
|
||||
|
||||
<div v-if="hasEditLink || hasLastUpdated" class="edit-info">
|
||||
<div v-if="hasEditLink" class="edit-link">
|
||||
<VPLink class="edit-link-button" :href="editLink.url" :no-icon="true">
|
||||
<span class="vpi-square-pen edit-link-icon" />
|
||||
{{ editLink.text }}
|
||||
</VPLink>
|
||||
</div>
|
||||
|
||||
<div v-if="hasLastUpdated" class="last-updated">
|
||||
<VPDocFooterLastUpdated />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav
|
||||
v-if="control.prev?.link || control.next?.link"
|
||||
class="prev-next"
|
||||
aria-labelledby="doc-footer-aria-label"
|
||||
>
|
||||
<span class="visually-hidden" id="doc-footer-aria-label">Pager</span>
|
||||
|
||||
<div class="pager">
|
||||
<VPLink
|
||||
v-if="control.prev?.link"
|
||||
class="pager-link prev"
|
||||
:href="control.prev.link"
|
||||
>
|
||||
<span
|
||||
class="desc"
|
||||
v-html="theme.docFooter?.prev || 'Previous page'"
|
||||
></span>
|
||||
<span class="title" v-html="control.prev.text"></span>
|
||||
</VPLink>
|
||||
</div>
|
||||
<div class="pager">
|
||||
<VPLink
|
||||
v-if="control.next?.link"
|
||||
class="pager-link next"
|
||||
:href="control.next.link"
|
||||
>
|
||||
<span
|
||||
class="desc"
|
||||
v-html="theme.docFooter?.next || 'Next page'"
|
||||
></span>
|
||||
<span class="title" v-html="control.next.text"></span>
|
||||
</VPLink>
|
||||
</div>
|
||||
</nav>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPDocFooter {
|
||||
margin-top: 64px;
|
||||
}
|
||||
|
||||
.edit-info {
|
||||
padding-bottom: 18px;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.edit-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-bottom: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-link-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 0;
|
||||
line-height: 32px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-brand-1);
|
||||
transition: color 0.25s;
|
||||
}
|
||||
|
||||
.edit-link-button:hover {
|
||||
color: var(--vp-c-brand-2);
|
||||
}
|
||||
|
||||
.edit-link-icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.prev-next {
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
padding-top: 24px;
|
||||
display: grid;
|
||||
grid-row-gap: 8px;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.prev-next {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-column-gap: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.pager-link {
|
||||
display: block;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 11px 16px 13px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
|
||||
.pager-link:hover {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.pager-link.next {
|
||||
margin-left: auto;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.desc {
|
||||
display: block;
|
||||
line-height: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.title {
|
||||
display: block;
|
||||
line-height: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-brand-1);
|
||||
transition: color 0.25s;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,50 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watchEffect, onMounted } from 'vue'
|
||||
import { useData } from '../composables/data'
|
||||
|
||||
const { theme, page, lang } = useData()
|
||||
|
||||
const date = computed(
|
||||
() => new Date(page.value.lastUpdated!)
|
||||
)
|
||||
const isoDatetime = computed(() => date.value.toISOString())
|
||||
const datetime = ref('')
|
||||
|
||||
// set time on mounted hook to avoid hydration mismatch due to
|
||||
// potential differences in timezones of the server and clients
|
||||
onMounted(() => {
|
||||
watchEffect(() => {
|
||||
datetime.value = new Intl.DateTimeFormat(
|
||||
theme.value.lastUpdated?.formatOptions?.forceLocale ? lang.value : undefined,
|
||||
theme.value.lastUpdated?.formatOptions ?? {
|
||||
dateStyle: 'short',
|
||||
timeStyle: 'short'
|
||||
}
|
||||
).format(date.value)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p class="VPLastUpdated">
|
||||
{{ theme.lastUpdated?.text || theme.lastUpdatedText || 'Last updated' }}:
|
||||
<time :datetime="isoDatetime">{{ datetime }}</time>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPLastUpdated {
|
||||
line-height: 24px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.VPLastUpdated {
|
||||
line-height: 32px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,61 @@
|
||||
<script setup lang="ts">
|
||||
import type { DefaultTheme } from 'vitepress/theme'
|
||||
|
||||
defineProps<{
|
||||
headers: DefaultTheme.OutlineItem[]
|
||||
root?: boolean
|
||||
}>()
|
||||
|
||||
function onClick({ target: el }: Event) {
|
||||
const id = (el as HTMLAnchorElement).href!.split('#')[1]
|
||||
const heading = document.getElementById(decodeURIComponent(id))
|
||||
heading?.focus({ preventScroll: true })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul class="VPDocOutlineItem" :class="root ? 'root' : 'nested'">
|
||||
<li v-for="{ children, link, title } in headers">
|
||||
<a class="outline-link" :href="link" @click="onClick" :title>
|
||||
{{ title }}
|
||||
</a>
|
||||
<template v-if="children?.length">
|
||||
<VPDocOutlineItem :headers="children" />
|
||||
</template>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.root {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.nested {
|
||||
padding-right: 16px;
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.outline-link {
|
||||
display: block;
|
||||
line-height: 32px;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: var(--vp-c-text-2);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
transition: color 0.5s;
|
||||
}
|
||||
|
||||
.outline-link:hover,
|
||||
.outline-link.active {
|
||||
color: var(--vp-c-text-1);
|
||||
transition: color 0.25s;
|
||||
}
|
||||
|
||||
.outline-link.nested {
|
||||
padding-left: 13px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,123 @@
|
||||
<script setup lang="ts">
|
||||
import type { DefaultTheme } from 'vitepress/theme'
|
||||
import VPImage from './VPImage.vue'
|
||||
import VPLink from './VPLink.vue'
|
||||
|
||||
defineProps<{
|
||||
icon?: DefaultTheme.FeatureIcon
|
||||
title: string
|
||||
details?: string
|
||||
link?: string
|
||||
linkText?: string
|
||||
rel?: string
|
||||
target?: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VPLink
|
||||
class="VPFeature"
|
||||
:href="link"
|
||||
:rel
|
||||
:target
|
||||
:no-icon="true"
|
||||
:tag="link ? 'a' : 'div'"
|
||||
>
|
||||
<article class="box">
|
||||
<div v-if="typeof icon === 'object' && icon.wrap" class="icon">
|
||||
<VPImage
|
||||
:image="icon"
|
||||
:alt="icon.alt"
|
||||
:height="icon.height || 48"
|
||||
:width="icon.width || 48"
|
||||
/>
|
||||
</div>
|
||||
<VPImage
|
||||
v-else-if="typeof icon === 'object'"
|
||||
:image="icon"
|
||||
:alt="icon.alt"
|
||||
:height="icon.height || 48"
|
||||
:width="icon.width || 48"
|
||||
/>
|
||||
<div v-else-if="icon" class="icon" v-html="icon"></div>
|
||||
<h2 class="title" v-html="title"></h2>
|
||||
<p v-if="details" class="details" v-html="details"></p>
|
||||
|
||||
<div v-if="linkText" class="link-text">
|
||||
<p class="link-text-value">
|
||||
{{ linkText }} <span class="vpi-arrow-right link-text-icon" />
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
</VPLink>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPFeature {
|
||||
display: block;
|
||||
border: 1px solid var(--vp-c-bg-soft);
|
||||
border-radius: 12px;
|
||||
height: 100%;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
transition: border-color 0.25s, background-color 0.25s;
|
||||
}
|
||||
|
||||
.VPFeature.link:hover {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 24px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.box > :deep(.VPImage) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 6px;
|
||||
background-color: var(--vp-c-default-soft);
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
font-size: 24px;
|
||||
transition: background-color 0.25s;
|
||||
}
|
||||
|
||||
.title {
|
||||
line-height: 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.details {
|
||||
flex-grow: 1;
|
||||
padding-top: 8px;
|
||||
line-height: 24px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.link-text {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.link-text-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.link-text-icon {
|
||||
margin-left: 6px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,121 @@
|
||||
<script setup lang="ts">
|
||||
import type { DefaultTheme } from 'vitepress/theme'
|
||||
import { computed } from 'vue'
|
||||
import VPFeature from './VPFeature.vue'
|
||||
|
||||
export interface Feature {
|
||||
icon?: DefaultTheme.FeatureIcon
|
||||
title: string
|
||||
details: string
|
||||
link?: string
|
||||
linkText?: string
|
||||
rel?: string
|
||||
target?: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
features: Feature[]
|
||||
}>()
|
||||
|
||||
const grid = computed(() => {
|
||||
const length = props.features.length
|
||||
|
||||
if (!length) {
|
||||
return
|
||||
} else if (length === 2) {
|
||||
return 'grid-2'
|
||||
} else if (length === 3) {
|
||||
return 'grid-3'
|
||||
} else if (length % 3 === 0) {
|
||||
return 'grid-6'
|
||||
} else if (length > 3) {
|
||||
return 'grid-4'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="features" class="VPFeatures">
|
||||
<div class="container">
|
||||
<div class="items">
|
||||
<div
|
||||
v-for="feature in features"
|
||||
:key="feature.title"
|
||||
class="item"
|
||||
:class="[grid]"
|
||||
>
|
||||
<VPFeature
|
||||
:icon="feature.icon"
|
||||
:title="feature.title"
|
||||
:details="feature.details"
|
||||
:link="feature.link"
|
||||
:link-text="feature.linkText"
|
||||
:rel="feature.rel"
|
||||
:target="feature.target"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPFeatures {
|
||||
position: relative;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.VPFeatures {
|
||||
padding: 0 48px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.VPFeatures {
|
||||
padding: 0 64px;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0 auto;
|
||||
max-width: 1152px;
|
||||
}
|
||||
|
||||
.items {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: -8px;
|
||||
}
|
||||
|
||||
.item {
|
||||
padding: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.item.grid-2,
|
||||
.item.grid-4,
|
||||
.item.grid-6 {
|
||||
width: calc(100% / 2);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.item.grid-2,
|
||||
.item.grid-4 {
|
||||
width: calc(100% / 2);
|
||||
}
|
||||
|
||||
.item.grid-3,
|
||||
.item.grid-6 {
|
||||
width: calc(100% / 3);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.item.grid-4 {
|
||||
width: calc(100% / 4);
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,137 @@
|
||||
<script lang="ts" setup generic="T extends DefaultTheme.NavItem">
|
||||
import type { DefaultTheme } from 'vitepress/theme'
|
||||
import { ref } from 'vue'
|
||||
import { useFlyout } from '../composables/flyout'
|
||||
import VPMenu from './VPMenu.vue'
|
||||
|
||||
defineProps<{
|
||||
icon?: string
|
||||
button?: string
|
||||
label?: string
|
||||
items?: T[]
|
||||
}>()
|
||||
|
||||
const open = ref(false)
|
||||
const el = ref<HTMLElement>()
|
||||
|
||||
useFlyout({ el, onBlur })
|
||||
|
||||
function onBlur() {
|
||||
open.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="VPFlyout"
|
||||
ref="el"
|
||||
@mouseenter="open = true"
|
||||
@mouseleave="open = false"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="button"
|
||||
aria-haspopup="true"
|
||||
:aria-expanded="open"
|
||||
:aria-label="label"
|
||||
@click="open = !open"
|
||||
>
|
||||
<span v-if="button || icon" class="text">
|
||||
<span v-if="icon" :class="[icon, 'option-icon']" />
|
||||
<span v-if="button" v-html="button"></span>
|
||||
<span class="vpi-chevron-down text-icon" />
|
||||
</span>
|
||||
|
||||
<span v-else class="vpi-more-horizontal icon" />
|
||||
</button>
|
||||
|
||||
<div class="menu">
|
||||
<VPMenu :items>
|
||||
<slot />
|
||||
</VPMenu>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPFlyout {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.VPFlyout:hover {
|
||||
color: var(--vp-c-brand-1);
|
||||
transition: color 0.25s;
|
||||
}
|
||||
|
||||
.VPFlyout:hover .text {
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.VPFlyout:hover .icon {
|
||||
fill: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.VPFlyout.active .text {
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.VPFlyout.active:hover .text {
|
||||
color: var(--vp-c-brand-2);
|
||||
}
|
||||
|
||||
.button[aria-expanded="false"] + .menu {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.VPFlyout:hover .menu,
|
||||
.button[aria-expanded="true"] + .menu {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
height: var(--vp-nav-height);
|
||||
color: var(--vp-c-text-1);
|
||||
transition: color 0.5s;
|
||||
}
|
||||
|
||||
.text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: var(--vp-nav-height);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-1);
|
||||
transition: color 0.25s;
|
||||
}
|
||||
|
||||
.option-icon {
|
||||
margin-right: 0px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.text-icon {
|
||||
margin-left: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 20px;
|
||||
transition: fill 0.25s;
|
||||
}
|
||||
|
||||
.menu {
|
||||
position: absolute;
|
||||
top: calc(var(--vp-nav-height) / 2 + 20px);
|
||||
right: 0;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.25s, visibility 0.25s, transform 0.25s;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,60 @@
|
||||
<script setup lang="ts">
|
||||
import { useData } from '../composables/data'
|
||||
import { useLayout } from '../composables/layout'
|
||||
|
||||
const { theme, frontmatter } = useData()
|
||||
const { hasSidebar } = useLayout()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer v-if="theme.footer && frontmatter.footer !== false" class="VPFooter" :class="{ 'has-sidebar': hasSidebar }">
|
||||
<div class="container">
|
||||
<p v-if="theme.footer.message" class="message" v-html="theme.footer.message"></p>
|
||||
<p v-if="theme.footer.copyright" class="copyright" v-html="theme.footer.copyright"></p>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPFooter {
|
||||
position: relative;
|
||||
z-index: var(--vp-z-index-footer);
|
||||
border-top: 1px solid var(--vp-c-gutter);
|
||||
padding: 32px 24px;
|
||||
background-color: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
.VPFooter.has-sidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.VPFooter :deep(a) {
|
||||
text-decoration-line: underline;
|
||||
text-underline-offset: 2px;
|
||||
transition: color 0.25s;
|
||||
}
|
||||
|
||||
.VPFooter :deep(a:hover) {
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.VPFooter {
|
||||
padding: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0 auto;
|
||||
max-width: var(--vp-layout-max-width);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.message,
|
||||
.copyright {
|
||||
line-height: 24px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
</style>
|
@ -0,0 +1,346 @@
|
||||
<script setup lang="ts">
|
||||
import type { DefaultTheme } from 'vitepress/theme'
|
||||
import { inject } from 'vue'
|
||||
import { layoutInfoInjectionKey } from '../composables/layout'
|
||||
import VPButton from './VPButton.vue'
|
||||
import VPImage from './VPImage.vue'
|
||||
|
||||
export interface HeroAction {
|
||||
theme?: 'brand' | 'alt'
|
||||
text: string
|
||||
link: string
|
||||
target?: string
|
||||
rel?: string
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
name?: string
|
||||
text?: string
|
||||
tagline?: string
|
||||
image?: DefaultTheme.ThemeableImage
|
||||
actions?: HeroAction[]
|
||||
}>()
|
||||
|
||||
const { heroImageSlotExists } = inject(layoutInfoInjectionKey)!
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="VPHero" :class="{ 'has-image': image || heroImageSlotExists }">
|
||||
<div class="container">
|
||||
<div class="main">
|
||||
<slot name="home-hero-info-before" />
|
||||
<slot name="home-hero-info">
|
||||
<h1 class="heading">
|
||||
<span v-if="name" v-html="name" class="name clip"></span>
|
||||
<span v-if="text" v-html="text" class="text"></span>
|
||||
</h1>
|
||||
<p v-if="tagline" v-html="tagline" class="tagline"></p>
|
||||
</slot>
|
||||
<slot name="home-hero-info-after" />
|
||||
|
||||
<div v-if="actions" class="actions">
|
||||
<div v-for="action in actions" :key="action.link" class="action">
|
||||
<VPButton
|
||||
tag="a"
|
||||
size="medium"
|
||||
:theme="action.theme"
|
||||
:text="action.text"
|
||||
:href="action.link"
|
||||
:target="action.target"
|
||||
:rel="action.rel"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<slot name="home-hero-actions-after" />
|
||||
</div>
|
||||
|
||||
<div v-if="image || heroImageSlotExists" class="image">
|
||||
<div class="image-container">
|
||||
<div class="image-bg" />
|
||||
<slot name="home-hero-image">
|
||||
<VPImage v-if="image" class="image-src" :image />
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPHero {
|
||||
margin-top: calc((var(--vp-nav-height) + var(--vp-layout-top-height, 0px)) * -1);
|
||||
padding: calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 48px) 24px 48px;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.VPHero {
|
||||
padding: calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 80px) 48px 64px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.VPHero {
|
||||
padding: calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 80px) 64px 64px;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0 auto;
|
||||
max-width: 1152px;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.container {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
order: 2;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.VPHero.has-image .container {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.VPHero.has-image .container {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.main {
|
||||
order: 1;
|
||||
width: calc((100% / 3) * 2);
|
||||
}
|
||||
|
||||
.VPHero.has-image .main {
|
||||
max-width: 592px;
|
||||
}
|
||||
}
|
||||
|
||||
.heading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.name,
|
||||
.text {
|
||||
width: fit-content;
|
||||
max-width: 392px;
|
||||
letter-spacing: -0.4px;
|
||||
line-height: 40px;
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.VPHero.has-image .name,
|
||||
.VPHero.has-image .text {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.name {
|
||||
color: var(--vp-home-hero-name-color);
|
||||
}
|
||||
|
||||
.clip {
|
||||
background: var(--vp-home-hero-name-background);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: var(--vp-home-hero-name-color);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.name,
|
||||
.text {
|
||||
max-width: 576px;
|
||||
line-height: 56px;
|
||||
font-size: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.name,
|
||||
.text {
|
||||
line-height: 64px;
|
||||
font-size: 56px;
|
||||
}
|
||||
|
||||
.VPHero.has-image .name,
|
||||
.VPHero.has-image .text {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tagline {
|
||||
padding-top: 8px;
|
||||
max-width: 392px;
|
||||
line-height: 28px;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
white-space: pre-wrap;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.VPHero.has-image .tagline {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.tagline {
|
||||
padding-top: 12px;
|
||||
max-width: 576px;
|
||||
line-height: 32px;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.tagline {
|
||||
line-height: 36px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.VPHero.has-image .tagline {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: -6px;
|
||||
padding-top: 24px;
|
||||
}
|
||||
|
||||
.VPHero.has-image .actions {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.actions {
|
||||
padding-top: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.VPHero.has-image .actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.action {
|
||||
flex-shrink: 0;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.image {
|
||||
order: 1;
|
||||
margin: -76px -24px -48px;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.image {
|
||||
margin: -108px -24px -48px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.image {
|
||||
flex-grow: 1;
|
||||
order: 2;
|
||||
margin: 0;
|
||||
min-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.image-container {
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
width: 320px;
|
||||
height: 320px;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.image-container {
|
||||
width: 392px;
|
||||
height: 392px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.image-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/*rtl:ignore*/
|
||||
transform: translate(-32px, -32px);
|
||||
}
|
||||
}
|
||||
|
||||
.image-bg {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
/*rtl:ignore*/
|
||||
left: 50%;
|
||||
border-radius: 50%;
|
||||
width: 192px;
|
||||
height: 192px;
|
||||
background-image: var(--vp-home-hero-image-background-image);
|
||||
filter: var(--vp-home-hero-image-filter);
|
||||
/*rtl:ignore*/
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.image-bg {
|
||||
width: 256px;
|
||||
height: 256px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.image-bg {
|
||||
width: 320px;
|
||||
height: 320px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.image-src) {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
/*rtl:ignore*/
|
||||
left: 50%;
|
||||
max-width: 192px;
|
||||
max-height: 192px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
/*rtl:ignore*/
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
:deep(.image-src) {
|
||||
max-width: 256px;
|
||||
max-height: 256px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
:deep(.image-src) {
|
||||
max-width: 320px;
|
||||
max-height: 320px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
import VPHomeHero from './VPHomeHero.vue'
|
||||
import VPHomeFeatures from './VPHomeFeatures.vue'
|
||||
import VPHomeContent from './VPHomeContent.vue'
|
||||
import { useData } from '../composables/data'
|
||||
|
||||
const { frontmatter, theme } = useData()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="VPHome"
|
||||
:class="{
|
||||
'external-link-icon-enabled': theme.externalLinkIcon
|
||||
}">
|
||||
<slot name="home-hero-before" />
|
||||
<VPHomeHero>
|
||||
<template #home-hero-info-before><slot name="home-hero-info-before" /></template>
|
||||
<template #home-hero-info><slot name="home-hero-info" /></template>
|
||||
<template #home-hero-info-after><slot name="home-hero-info-after" /></template>
|
||||
<template #home-hero-actions-after><slot name="home-hero-actions-after" /></template>
|
||||
<template #home-hero-image><slot name="home-hero-image" /></template>
|
||||
</VPHomeHero>
|
||||
<slot name="home-hero-after" />
|
||||
|
||||
<slot name="home-features-before" />
|
||||
<VPHomeFeatures />
|
||||
<slot name="home-features-after" />
|
||||
|
||||
<VPHomeContent v-if="frontmatter.markdownStyles !== false">
|
||||
<Content />
|
||||
</VPHomeContent>
|
||||
<Content v-else />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPHome {
|
||||
margin-bottom: 96px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.VPHome {
|
||||
margin-bottom: 128px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,55 @@
|
||||
<script setup lang="ts">
|
||||
import { useWindowSize } from '@vueuse/core'
|
||||
|
||||
const { width: vw } = useWindowSize({
|
||||
initialWidth: 0,
|
||||
includeScrollbar: false
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="vp-doc container"
|
||||
:style="vw ? { '--vp-offset': `calc(50% - ${vw / 2}px)` } : {}"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
margin: auto;
|
||||
width: 100%;
|
||||
max-width: 1280px;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.container {
|
||||
padding: 0 48px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.container {
|
||||
width: 100%;
|
||||
padding: 0 64px;
|
||||
}
|
||||
}
|
||||
|
||||
.vp-doc :deep(.VPHomeSponsors),
|
||||
.vp-doc :deep(.VPTeamPage) {
|
||||
margin-left: var(--vp-offset, calc(50% - 50vw));
|
||||
margin-right: var(--vp-offset, calc(50% - 50vw));
|
||||
}
|
||||
|
||||
.vp-doc :deep(.VPHomeSponsors h2) {
|
||||
border-top: none;
|
||||
letter-spacing: normal;
|
||||
}
|
||||
|
||||
.vp-doc :deep(.VPHomeSponsors a),
|
||||
.vp-doc :deep(.VPTeamPage a) {
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { useData } from '../composables/data'
|
||||
import VPFeatures from './VPFeatures.vue'
|
||||
|
||||
const { frontmatter: fm } = useData()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VPFeatures
|
||||
v-if="fm.features"
|
||||
class="VPHomeFeatures"
|
||||
:features="fm.features"
|
||||
/>
|
||||
</template>
|
@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { useData } from '../composables/data'
|
||||
import VPHero from './VPHero.vue'
|
||||
|
||||
const { frontmatter: fm } = useData()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VPHero
|
||||
v-if="fm.hero"
|
||||
class="VPHomeHero"
|
||||
:name="fm.hero.name"
|
||||
:text="fm.hero.text"
|
||||
:tagline="fm.hero.tagline"
|
||||
:image="fm.hero.image"
|
||||
:actions="fm.hero.actions"
|
||||
>
|
||||
<template #home-hero-info-before><slot name="home-hero-info-before" /></template>
|
||||
<template #home-hero-info><slot name="home-hero-info" /></template>
|
||||
<template #home-hero-info-after><slot name="home-hero-info-after" /></template>
|
||||
<template #home-hero-actions-after><slot name="home-hero-actions-after" /></template>
|
||||
<template #home-hero-image><slot name="home-hero-image" /></template>
|
||||
</VPHero>
|
||||
</template>
|
@ -0,0 +1,116 @@
|
||||
<script setup lang="ts">
|
||||
import VPButton from './VPButton.vue'
|
||||
import VPSponsors from './VPSponsors.vue'
|
||||
|
||||
export interface Sponsors {
|
||||
tier: string
|
||||
size?: 'medium' | 'big'
|
||||
items: Sponsor[]
|
||||
}
|
||||
|
||||
export interface Sponsor {
|
||||
name: string
|
||||
img: string
|
||||
url: string
|
||||
}
|
||||
interface Props {
|
||||
message?: string
|
||||
actionText?: string
|
||||
actionLink?: string
|
||||
data: Sponsors[]
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
actionText: 'Become a sponsor'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="VPHomeSponsors">
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<div class="love">
|
||||
<span class="vpi-heart icon" />
|
||||
</div>
|
||||
<h2 v-if="message" class="message">{{ message }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="sponsors">
|
||||
<VPSponsors :data />
|
||||
</div>
|
||||
|
||||
<div v-if="actionLink" class="action">
|
||||
<VPButton theme="sponsor" :text="actionText" :href="actionLink" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPHomeSponsors {
|
||||
border-top: 1px solid var(--vp-c-gutter);
|
||||
padding-top: 88px !important;
|
||||
}
|
||||
|
||||
.VPHomeSponsors {
|
||||
margin: 96px 0;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.VPHomeSponsors {
|
||||
margin: 128px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.VPHomeSponsors {
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.VPHomeSponsors {
|
||||
padding: 0 48px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.VPHomeSponsors {
|
||||
padding: 0 64px;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0 auto;
|
||||
max-width: 1152px;
|
||||
}
|
||||
|
||||
.love {
|
||||
margin: 0 auto;
|
||||
width: fit-content;
|
||||
font-size: 28px;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin: 0 auto;
|
||||
padding-top: 10px;
|
||||
max-width: 320px;
|
||||
text-align: center;
|
||||
line-height: 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.sponsors {
|
||||
padding-top: 32px;
|
||||
}
|
||||
|
||||
.action {
|
||||
padding-top: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
import type { DefaultTheme } from 'vitepress/theme'
|
||||
import { withBase } from 'vitepress'
|
||||
|
||||
defineProps<{
|
||||
image: DefaultTheme.ThemeableImage
|
||||
alt?: string
|
||||
}>()
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="image">
|
||||
<img
|
||||
v-if="typeof image === 'string' || 'src' in image"
|
||||
class="vp-image"
|
||||
v-bind="typeof image === 'string' ? $attrs : { ...image, ...$attrs }"
|
||||
:src="withBase(typeof image === 'string' ? image : image.src)"
|
||||
:alt="alt ?? (typeof image === 'string' ? '' : image.alt || '')"
|
||||
/>
|
||||
<template v-else>
|
||||
<VPImage
|
||||
class="vp-image--dark"
|
||||
:image="image.dark"
|
||||
:alt="image.alt"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
<VPImage
|
||||
class="vp-image--light"
|
||||
:image="image.light"
|
||||
:alt="image.alt"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
html:not(.dark) .vp-image--dark {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dark .vp-image--dark {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,37 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import { normalizeLink } from '../support/utils'
|
||||
import { EXTERNAL_URL_RE } from '../../shared'
|
||||
|
||||
const props = defineProps<{
|
||||
tag?: string
|
||||
href?: string
|
||||
noIcon?: boolean
|
||||
target?: string
|
||||
rel?: string
|
||||
}>()
|
||||
|
||||
const tag = computed(() => props.tag ?? (props.href ? 'a' : 'span'))
|
||||
const isExternal = computed(
|
||||
() =>
|
||||
(props.href && EXTERNAL_URL_RE.test(props.href)) ||
|
||||
props.target === '_blank'
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="tag"
|
||||
class="VPLink"
|
||||
:class="{
|
||||
link: href,
|
||||
'vp-external-link-icon': isExternal,
|
||||
'no-icon': noIcon
|
||||
}"
|
||||
:href="href ? normalizeLink(href) : undefined"
|
||||
:target="target ?? (isExternal ? '_blank' : undefined)"
|
||||
:rel="rel ?? (isExternal ? 'noreferrer' : undefined)"
|
||||
>
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
@ -0,0 +1,150 @@
|
||||
<script lang="ts" setup>
|
||||
import { useWindowScroll } from '@vueuse/core'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useData } from '../composables/data'
|
||||
import { useLayout } from '../composables/layout'
|
||||
import VPLocalNavOutlineDropdown from './VPLocalNavOutlineDropdown.vue'
|
||||
|
||||
defineProps<{
|
||||
open: boolean
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
(e: 'open-menu'): void
|
||||
}>()
|
||||
|
||||
const { theme } = useData()
|
||||
const { isHome, hasSidebar, headers, hasLocalNav } = useLayout()
|
||||
const { y } = useWindowScroll()
|
||||
|
||||
const navHeight = ref(0)
|
||||
|
||||
onMounted(() => {
|
||||
navHeight.value = parseInt(
|
||||
getComputedStyle(document.documentElement).getPropertyValue(
|
||||
'--vp-nav-height'
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
const classes = computed(() => {
|
||||
return {
|
||||
VPLocalNav: true,
|
||||
'has-sidebar': hasSidebar.value,
|
||||
empty: !hasLocalNav.value,
|
||||
fixed: !hasLocalNav.value && !hasSidebar.value
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="!isHome && (hasLocalNav || hasSidebar || y >= navHeight)"
|
||||
:class="classes"
|
||||
>
|
||||
<div class="container">
|
||||
<button
|
||||
v-if="hasSidebar"
|
||||
class="menu"
|
||||
:aria-expanded="open"
|
||||
aria-controls="VPSidebarNav"
|
||||
@click="$emit('open-menu')"
|
||||
>
|
||||
<span class="vpi-align-left menu-icon"></span>
|
||||
<span class="menu-text">
|
||||
{{ theme.sidebarMenuLabel || 'Menu' }}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<VPLocalNavOutlineDropdown :headers :navHeight />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPLocalNav {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
/*rtl:ignore*/
|
||||
left: 0;
|
||||
z-index: var(--vp-z-index-local-nav);
|
||||
border-bottom: 1px solid var(--vp-c-gutter);
|
||||
padding-top: var(--vp-layout-top-height, 0px);
|
||||
width: 100%;
|
||||
background-color: var(--vp-local-nav-bg-color);
|
||||
}
|
||||
|
||||
.VPLocalNav.fixed {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.VPLocalNav {
|
||||
top: var(--vp-nav-height);
|
||||
}
|
||||
|
||||
.VPLocalNav.has-sidebar {
|
||||
padding-left: var(--vp-sidebar-width);
|
||||
}
|
||||
|
||||
.VPLocalNav.empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.VPLocalNav {
|
||||
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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 24px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-2);
|
||||
transition: color 0.5s;
|
||||
}
|
||||
|
||||
.menu:hover {
|
||||
color: var(--vp-c-text-1);
|
||||
transition: color 0.25s;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.menu {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
margin-right: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.menu,
|
||||
:deep(.VPLocalNavOutlineDropdown > button) {
|
||||
padding: 12px 24px 11px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.menu,
|
||||
:deep(.VPLocalNavOutlineDropdown > button) {
|
||||
padding: 12px 32px 11px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,192 @@
|
||||
<script setup lang="ts">
|
||||
import { onKeyStroke } from '@vueuse/core'
|
||||
import { onContentUpdated } from 'vitepress'
|
||||
import type { DefaultTheme } from 'vitepress/theme'
|
||||
import { nextTick, ref, watch } from 'vue'
|
||||
import { useData } from '../composables/data'
|
||||
import { resolveTitle } from '../composables/outline'
|
||||
import VPDocOutlineItem from './VPDocOutlineItem.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
headers: DefaultTheme.OutlineItem[]
|
||||
navHeight: number
|
||||
}>()
|
||||
|
||||
const { theme } = useData()
|
||||
const open = ref(false)
|
||||
const vh = ref(0)
|
||||
const main = ref<HTMLDivElement>()
|
||||
const items = ref<HTMLDivElement>()
|
||||
|
||||
function closeOnClickOutside(e: Event) {
|
||||
if (!main.value?.contains(e.target as Node)) {
|
||||
open.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(open, (value) => {
|
||||
if (value) {
|
||||
document.addEventListener('click', closeOnClickOutside)
|
||||
return
|
||||
}
|
||||
document.removeEventListener('click', closeOnClickOutside)
|
||||
})
|
||||
|
||||
onKeyStroke('Escape', () => {
|
||||
open.value = false
|
||||
})
|
||||
|
||||
onContentUpdated(() => {
|
||||
open.value = false
|
||||
})
|
||||
|
||||
function toggle() {
|
||||
open.value = !open.value
|
||||
vh.value = window.innerHeight + Math.min(window.scrollY - props.navHeight, 0)
|
||||
}
|
||||
|
||||
function onItemClick(e: Event) {
|
||||
if ((e.target as HTMLElement).classList.contains('outline-link')) {
|
||||
// disable animation on hash navigation when page jumps
|
||||
if (items.value) {
|
||||
items.value.style.transition = 'none'
|
||||
}
|
||||
nextTick(() => {
|
||||
open.value = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToTop() {
|
||||
open.value = false
|
||||
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="VPLocalNavOutlineDropdown"
|
||||
:style="{ '--vp-vh': vh + 'px' }"
|
||||
ref="main"
|
||||
>
|
||||
<button @click="toggle" :class="{ open }" v-if="headers.length > 0">
|
||||
<span class="menu-text">{{ resolveTitle(theme) }}</span>
|
||||
<span class="vpi-chevron-right icon" />
|
||||
</button>
|
||||
<button @click="scrollToTop" v-else>
|
||||
{{ theme.returnToTopLabel || 'Return to top' }}
|
||||
</button>
|
||||
<Transition name="flyout">
|
||||
<div v-if="open" ref="items" class="items" @click="onItemClick">
|
||||
<div class="header">
|
||||
<a class="top-link" href="#" @click="scrollToTop">
|
||||
{{ theme.returnToTopLabel || 'Return to top' }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="outline">
|
||||
<VPDocOutlineItem :headers />
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPLocalNavOutlineDropdown button {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
color: var(--vp-c-text-2);
|
||||
transition: color 0.5s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.VPLocalNavOutlineDropdown button:hover {
|
||||
color: var(--vp-c-text-1);
|
||||
transition: color 0.25s;
|
||||
}
|
||||
|
||||
.VPLocalNavOutlineDropdown button.open {
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-left: 2px;
|
||||
font-size: 14px;
|
||||
transform: rotate(0) /*rtl:rotate(180deg)*/;
|
||||
transition: transform 0.25s;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.VPLocalNavOutlineDropdown button {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.open > .icon {
|
||||
/*rtl:ignore*/
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.items {
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
right: 16px;
|
||||
left: 16px;
|
||||
display: grid;
|
||||
gap: 1px;
|
||||
border: 1px solid var(--vp-c-border);
|
||||
border-radius: 8px;
|
||||
background-color: var(--vp-c-gutter);
|
||||
max-height: calc(var(--vp-vh, 100vh) - 86px);
|
||||
overflow: hidden auto;
|
||||
box-shadow: var(--vp-shadow-3);
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.items {
|
||||
right: auto;
|
||||
left: calc(var(--vp-sidebar-width) + 32px);
|
||||
width: 320px;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.top-link {
|
||||
display: block;
|
||||
padding: 0 16px;
|
||||
line-height: 48px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.outline {
|
||||
padding: 8px 0;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.flyout-enter-active {
|
||||
transition: all 0.2s ease-out;
|
||||
}
|
||||
|
||||
.flyout-leave-active {
|
||||
transition: all 0.15s ease-in;
|
||||
}
|
||||
|
||||
.flyout-enter-from,
|
||||
.flyout-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-16px);
|
||||
}
|
||||
</style>
|
@ -0,0 +1,877 @@
|
||||
<script lang="ts" setup>
|
||||
import localSearchIndex from '@localSearchIndex'
|
||||
import {
|
||||
computedAsync,
|
||||
debouncedWatch,
|
||||
onKeyStroke,
|
||||
useEventListener,
|
||||
useLocalStorage,
|
||||
useScrollLock,
|
||||
useSessionStorage
|
||||
} from '@vueuse/core'
|
||||
import { useFocusTrap } from '@vueuse/integrations/useFocusTrap'
|
||||
import Mark from 'mark.js/src/vanilla.js'
|
||||
import MiniSearch, { type SearchResult } from 'minisearch'
|
||||
import { dataSymbol, inBrowser, useRouter } from 'vitepress'
|
||||
import {
|
||||
computed,
|
||||
createApp,
|
||||
markRaw,
|
||||
nextTick,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
ref,
|
||||
shallowRef,
|
||||
watch,
|
||||
watchEffect,
|
||||
type Ref
|
||||
} from 'vue'
|
||||
import type { ModalTranslations } from '../../../../types/local-search'
|
||||
import { pathToFile } from '../../app/utils'
|
||||
import { escapeRegExp } from '../../shared'
|
||||
import { useData } from '../composables/data'
|
||||
import { LRUCache } from '../support/lru'
|
||||
import { createSearchTranslate } from '../support/translation'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'close'): void
|
||||
}>()
|
||||
|
||||
const el = shallowRef<HTMLElement>()
|
||||
const resultsEl = shallowRef<HTMLElement>()
|
||||
|
||||
/* Search */
|
||||
|
||||
const searchIndexData = shallowRef(localSearchIndex)
|
||||
|
||||
// hmr
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.accept('@localSearchIndex', (m) => {
|
||||
if (m) {
|
||||
searchIndexData.value = m.default
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
interface Result {
|
||||
title: string
|
||||
titles: string[]
|
||||
text?: string
|
||||
}
|
||||
|
||||
const vitePressData = useData()
|
||||
const { activate } = useFocusTrap(el, {
|
||||
immediate: true,
|
||||
allowOutsideClick: true,
|
||||
clickOutsideDeactivates: true,
|
||||
escapeDeactivates: true
|
||||
})
|
||||
const { localeIndex, theme } = vitePressData
|
||||
const searchIndex = computedAsync(async () =>
|
||||
markRaw(
|
||||
MiniSearch.loadJSON<Result>(
|
||||
(await searchIndexData.value[localeIndex.value]?.())?.default,
|
||||
{
|
||||
fields: ['title', 'titles', 'text'],
|
||||
storeFields: ['title', 'titles'],
|
||||
searchOptions: {
|
||||
fuzzy: 0.2,
|
||||
prefix: true,
|
||||
boost: { title: 4, text: 2, titles: 1 },
|
||||
...(theme.value.search?.provider === 'local' &&
|
||||
theme.value.search.options?.miniSearch?.searchOptions)
|
||||
},
|
||||
...(theme.value.search?.provider === 'local' &&
|
||||
theme.value.search.options?.miniSearch?.options)
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
const disableQueryPersistence = computed(() => {
|
||||
return (
|
||||
theme.value.search?.provider === 'local' &&
|
||||
theme.value.search.options?.disableQueryPersistence === true
|
||||
)
|
||||
})
|
||||
|
||||
const filterText = disableQueryPersistence.value
|
||||
? ref('')
|
||||
: useSessionStorage('vitepress:local-search-filter', '')
|
||||
|
||||
const showDetailedList = useLocalStorage(
|
||||
'vitepress:local-search-detailed-list',
|
||||
theme.value.search?.provider === 'local' &&
|
||||
theme.value.search.options?.detailedView === true
|
||||
)
|
||||
|
||||
const disableDetailedView = computed(() => {
|
||||
return (
|
||||
theme.value.search?.provider === 'local' &&
|
||||
(theme.value.search.options?.disableDetailedView === true ||
|
||||
theme.value.search.options?.detailedView === false)
|
||||
)
|
||||
})
|
||||
|
||||
const buttonText = computed(() => {
|
||||
const options = theme.value.search?.options ?? theme.value.algolia
|
||||
|
||||
return (
|
||||
options?.locales?.[localeIndex.value]?.translations?.button?.buttonText ||
|
||||
options?.translations?.button?.buttonText ||
|
||||
'Search'
|
||||
)
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
if (disableDetailedView.value) {
|
||||
showDetailedList.value = false
|
||||
}
|
||||
})
|
||||
|
||||
const results: Ref<(SearchResult & Result)[]> = shallowRef([])
|
||||
|
||||
const enableNoResults = ref(false)
|
||||
|
||||
watch(filterText, () => {
|
||||
enableNoResults.value = false
|
||||
})
|
||||
|
||||
const mark = computedAsync(async () => {
|
||||
if (!resultsEl.value) return
|
||||
return markRaw(new Mark(resultsEl.value))
|
||||
}, null)
|
||||
|
||||
const cache = new LRUCache<string, Map<string, string>>(16) // 16 files
|
||||
|
||||
debouncedWatch(
|
||||
() => [searchIndex.value, filterText.value, showDetailedList.value] as const,
|
||||
async ([index, filterTextValue, showDetailedListValue], old, onCleanup) => {
|
||||
if (old?.[0] !== index) {
|
||||
// in case of hmr
|
||||
cache.clear()
|
||||
}
|
||||
|
||||
let canceled = false
|
||||
onCleanup(() => {
|
||||
canceled = true
|
||||
})
|
||||
|
||||
if (!index) return
|
||||
|
||||
// Search
|
||||
results.value = index
|
||||
.search(filterTextValue)
|
||||
.slice(0, 16) as (SearchResult & Result)[]
|
||||
enableNoResults.value = true
|
||||
|
||||
// Highlighting
|
||||
const mods = showDetailedListValue
|
||||
? await Promise.all(results.value.map((r) => fetchExcerpt(r.id)))
|
||||
: []
|
||||
if (canceled) return
|
||||
for (const { id, mod } of mods) {
|
||||
const mapId = id.slice(0, id.indexOf('#'))
|
||||
let map = cache.get(mapId)
|
||||
if (map) continue
|
||||
map = new Map()
|
||||
cache.set(mapId, map)
|
||||
const comp = mod.default ?? mod
|
||||
if (comp?.render || comp?.setup) {
|
||||
const app = createApp(comp)
|
||||
// Silence warnings about missing components
|
||||
app.config.warnHandler = () => {}
|
||||
app.provide(dataSymbol, vitePressData)
|
||||
Object.defineProperties(app.config.globalProperties, {
|
||||
$frontmatter: {
|
||||
get() {
|
||||
return vitePressData.frontmatter.value
|
||||
}
|
||||
},
|
||||
$params: {
|
||||
get() {
|
||||
return vitePressData.page.value.params
|
||||
}
|
||||
}
|
||||
})
|
||||
const div = document.createElement('div')
|
||||
app.mount(div)
|
||||
const headings = div.querySelectorAll('h1, h2, h3, h4, h5, h6')
|
||||
headings.forEach((el) => {
|
||||
const href = el.querySelector('a')?.getAttribute('href')
|
||||
const anchor = href?.startsWith('#') && href.slice(1)
|
||||
if (!anchor) return
|
||||
let html = ''
|
||||
while ((el = el.nextElementSibling!) && !/^h[1-6]$/i.test(el.tagName))
|
||||
html += el.outerHTML
|
||||
map!.set(anchor, html)
|
||||
})
|
||||
app.unmount()
|
||||
}
|
||||
if (canceled) return
|
||||
}
|
||||
|
||||
const terms = new Set<string>()
|
||||
|
||||
results.value = results.value.map((r) => {
|
||||
const [id, anchor] = r.id.split('#')
|
||||
const map = cache.get(id)
|
||||
const text = map?.get(anchor) ?? ''
|
||||
for (const term in r.match) {
|
||||
terms.add(term)
|
||||
}
|
||||
return { ...r, text }
|
||||
})
|
||||
|
||||
await nextTick()
|
||||
if (canceled) return
|
||||
|
||||
await new Promise((r) => {
|
||||
mark.value?.unmark({
|
||||
done: () => {
|
||||
mark.value?.markRegExp(formMarkRegex(terms), { done: r })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const excerpts = el.value?.querySelectorAll('.result .excerpt') ?? []
|
||||
for (const excerpt of excerpts) {
|
||||
excerpt
|
||||
.querySelector('mark[data-markjs="true"]')
|
||||
?.scrollIntoView({ block: 'center' })
|
||||
}
|
||||
// FIXME: without this whole page scrolls to the bottom
|
||||
resultsEl.value?.firstElementChild?.scrollIntoView({ block: 'start' })
|
||||
},
|
||||
{ debounce: 200, immediate: true }
|
||||
)
|
||||
|
||||
async function fetchExcerpt(id: string) {
|
||||
const file = pathToFile(id.slice(0, id.indexOf('#')))
|
||||
try {
|
||||
if (!file) throw new Error(`Cannot find file for id: ${id}`)
|
||||
return { id, mod: await import(/*@vite-ignore*/ file) }
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return { id, mod: {} }
|
||||
}
|
||||
}
|
||||
|
||||
/* Search input focus */
|
||||
|
||||
const searchInput = ref<HTMLInputElement>()
|
||||
const disableReset = computed(() => {
|
||||
return filterText.value?.length <= 0
|
||||
})
|
||||
function focusSearchInput(select = true) {
|
||||
searchInput.value?.focus()
|
||||
select && searchInput.value?.select()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
focusSearchInput()
|
||||
})
|
||||
|
||||
function onSearchBarClick(event: PointerEvent) {
|
||||
if (event.pointerType === 'mouse') {
|
||||
focusSearchInput()
|
||||
}
|
||||
}
|
||||
|
||||
/* Search keyboard selection */
|
||||
|
||||
const selectedIndex = ref(-1)
|
||||
const disableMouseOver = ref(true)
|
||||
|
||||
watch(results, (r) => {
|
||||
selectedIndex.value = r.length ? 0 : -1
|
||||
scrollToSelectedResult()
|
||||
})
|
||||
|
||||
function scrollToSelectedResult() {
|
||||
nextTick(() => {
|
||||
const selectedEl = document.querySelector('.result.selected')
|
||||
selectedEl?.scrollIntoView({ block: 'nearest' })
|
||||
})
|
||||
}
|
||||
|
||||
onKeyStroke('ArrowUp', (event) => {
|
||||
event.preventDefault()
|
||||
selectedIndex.value--
|
||||
if (selectedIndex.value < 0) {
|
||||
selectedIndex.value = results.value.length - 1
|
||||
}
|
||||
disableMouseOver.value = true
|
||||
scrollToSelectedResult()
|
||||
})
|
||||
|
||||
onKeyStroke('ArrowDown', (event) => {
|
||||
event.preventDefault()
|
||||
selectedIndex.value++
|
||||
if (selectedIndex.value >= results.value.length) {
|
||||
selectedIndex.value = 0
|
||||
}
|
||||
disableMouseOver.value = true
|
||||
scrollToSelectedResult()
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
onKeyStroke('Enter', (e) => {
|
||||
if (e.isComposing) return
|
||||
|
||||
if (e.target instanceof HTMLButtonElement && e.target.type !== 'submit')
|
||||
return
|
||||
|
||||
const selectedPackage = results.value[selectedIndex.value]
|
||||
if (e.target instanceof HTMLInputElement && !selectedPackage) {
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
if (selectedPackage) {
|
||||
router.go(selectedPackage.id)
|
||||
emit('close')
|
||||
}
|
||||
})
|
||||
|
||||
onKeyStroke('Escape', () => {
|
||||
emit('close')
|
||||
})
|
||||
|
||||
// Translations
|
||||
const defaultTranslations: { modal: ModalTranslations } = {
|
||||
modal: {
|
||||
displayDetails: 'Display detailed list',
|
||||
resetButtonTitle: 'Reset search',
|
||||
backButtonTitle: 'Close search',
|
||||
noResultsText: 'No results for',
|
||||
footer: {
|
||||
selectText: 'to select',
|
||||
selectKeyAriaLabel: 'enter',
|
||||
navigateText: 'to navigate',
|
||||
navigateUpKeyAriaLabel: 'up arrow',
|
||||
navigateDownKeyAriaLabel: 'down arrow',
|
||||
closeText: 'to close',
|
||||
closeKeyAriaLabel: 'escape'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const translate = createSearchTranslate(defaultTranslations)
|
||||
|
||||
// Back
|
||||
|
||||
onMounted(() => {
|
||||
// Prevents going to previous site
|
||||
window.history.pushState(null, '', null)
|
||||
})
|
||||
|
||||
useEventListener('popstate', (event) => {
|
||||
event.preventDefault()
|
||||
emit('close')
|
||||
})
|
||||
|
||||
/** Lock body */
|
||||
const isLocked = useScrollLock(inBrowser ? document.body : null)
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
isLocked.value = true
|
||||
nextTick().then(() => activate())
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
isLocked.value = false
|
||||
})
|
||||
|
||||
function resetSearch() {
|
||||
filterText.value = ''
|
||||
nextTick().then(() => focusSearchInput(false))
|
||||
}
|
||||
|
||||
function formMarkRegex(terms: Set<string>) {
|
||||
return new RegExp(
|
||||
[...terms]
|
||||
.sort((a, b) => b.length - a.length)
|
||||
.map((term) => `(${escapeRegExp(term)})`)
|
||||
.join('|'),
|
||||
'gi'
|
||||
)
|
||||
}
|
||||
|
||||
function onMouseMove(e: MouseEvent) {
|
||||
if (!disableMouseOver.value) return
|
||||
const el = (e.target as HTMLElement)?.closest<HTMLAnchorElement>('.result')
|
||||
const index = Number.parseInt(el?.dataset.index!)
|
||||
if (index >= 0 && index !== selectedIndex.value) {
|
||||
selectedIndex.value = index
|
||||
}
|
||||
disableMouseOver.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div
|
||||
ref="el"
|
||||
role="button"
|
||||
:aria-owns="results?.length ? 'localsearch-list' : undefined"
|
||||
aria-expanded="true"
|
||||
aria-haspopup="listbox"
|
||||
aria-labelledby="localsearch-label"
|
||||
class="VPLocalSearchBox"
|
||||
>
|
||||
<div class="backdrop" @click="$emit('close')" />
|
||||
|
||||
<div class="shell">
|
||||
<form
|
||||
class="search-bar"
|
||||
@pointerup="onSearchBarClick($event)"
|
||||
@submit.prevent=""
|
||||
>
|
||||
<label
|
||||
:title="buttonText"
|
||||
id="localsearch-label"
|
||||
for="localsearch-input"
|
||||
>
|
||||
<span aria-hidden="true" class="vpi-search search-icon local-search-icon" />
|
||||
</label>
|
||||
<div class="search-actions before">
|
||||
<button
|
||||
class="back-button"
|
||||
:title="translate('modal.backButtonTitle')"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
<span class="vpi-arrow-left local-search-icon" />
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
ref="searchInput"
|
||||
v-model="filterText"
|
||||
:aria-activedescendant="selectedIndex > -1 ? ('localsearch-item-' + selectedIndex) : undefined"
|
||||
aria-autocomplete="both"
|
||||
:aria-controls="results?.length ? 'localsearch-list' : undefined"
|
||||
aria-labelledby="localsearch-label"
|
||||
autocapitalize="off"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
class="search-input"
|
||||
id="localsearch-input"
|
||||
enterkeyhint="go"
|
||||
maxlength="64"
|
||||
:placeholder="buttonText"
|
||||
spellcheck="false"
|
||||
type="search"
|
||||
/>
|
||||
<div class="search-actions">
|
||||
<button
|
||||
v-if="!disableDetailedView"
|
||||
class="toggle-layout-button"
|
||||
type="button"
|
||||
:class="{ 'detailed-list': showDetailedList }"
|
||||
:title="translate('modal.displayDetails')"
|
||||
@click="
|
||||
selectedIndex > -1 && (showDetailedList = !showDetailedList)
|
||||
"
|
||||
>
|
||||
<span class="vpi-layout-list local-search-icon" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="clear-button"
|
||||
type="reset"
|
||||
:disabled="disableReset"
|
||||
:title="translate('modal.resetButtonTitle')"
|
||||
@click="resetSearch"
|
||||
>
|
||||
<span class="vpi-delete local-search-icon" />
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ul
|
||||
ref="resultsEl"
|
||||
:id="results?.length ? 'localsearch-list' : undefined"
|
||||
:role="results?.length ? 'listbox' : undefined"
|
||||
:aria-labelledby="results?.length ? 'localsearch-label' : undefined"
|
||||
class="results"
|
||||
@mousemove="onMouseMove"
|
||||
>
|
||||
<li
|
||||
v-for="(p, index) in results"
|
||||
:key="p.id"
|
||||
:id="'localsearch-item-' + index"
|
||||
:aria-selected="selectedIndex === index ? 'true' : 'false'"
|
||||
role="option"
|
||||
>
|
||||
<a
|
||||
:href="p.id"
|
||||
class="result"
|
||||
:class="{
|
||||
selected: selectedIndex === index
|
||||
}"
|
||||
:aria-label="[...p.titles, p.title].join(' > ')"
|
||||
@mouseenter="!disableMouseOver && (selectedIndex = index)"
|
||||
@focusin="selectedIndex = index"
|
||||
@click="$emit('close')"
|
||||
:data-index="index"
|
||||
>
|
||||
<div>
|
||||
<div class="titles">
|
||||
<span class="title-icon">#</span>
|
||||
<span
|
||||
v-for="(t, index) in p.titles"
|
||||
:key="index"
|
||||
class="title"
|
||||
>
|
||||
<span class="text" v-html="t" />
|
||||
<span class="vpi-chevron-right local-search-icon" />
|
||||
</span>
|
||||
<span class="title main">
|
||||
<span class="text" v-html="p.title" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="showDetailedList" class="excerpt-wrapper">
|
||||
<div v-if="p.text" class="excerpt" inert>
|
||||
<div class="vp-doc" v-html="p.text" />
|
||||
</div>
|
||||
<div class="excerpt-gradient-bottom" />
|
||||
<div class="excerpt-gradient-top" />
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
v-if="filterText && !results.length && enableNoResults"
|
||||
class="no-results"
|
||||
>
|
||||
{{ translate('modal.noResultsText') }} "<strong>{{ filterText }}</strong
|
||||
>"
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="search-keyboard-shortcuts">
|
||||
<span>
|
||||
<kbd :aria-label="translate('modal.footer.navigateUpKeyAriaLabel')">
|
||||
<span class="vpi-arrow-up navigate-icon" />
|
||||
</kbd>
|
||||
<kbd :aria-label="translate('modal.footer.navigateDownKeyAriaLabel')">
|
||||
<span class="vpi-arrow-down navigate-icon" />
|
||||
</kbd>
|
||||
{{ translate('modal.footer.navigateText') }}
|
||||
</span>
|
||||
<span>
|
||||
<kbd :aria-label="translate('modal.footer.selectKeyAriaLabel')">
|
||||
<span class="vpi-corner-down-left navigate-icon" />
|
||||
</kbd>
|
||||
{{ translate('modal.footer.selectText') }}
|
||||
</span>
|
||||
<span>
|
||||
<kbd :aria-label="translate('modal.footer.closeKeyAriaLabel')">esc</kbd>
|
||||
{{ translate('modal.footer.closeText') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPLocalSearchBox {
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--vp-backdrop-bg-color);
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
|
||||
.shell {
|
||||
position: relative;
|
||||
padding: 12px;
|
||||
margin: 64px auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
background: var(--vp-local-search-bg);
|
||||
width: min(100vw - 60px, 900px);
|
||||
height: min-content;
|
||||
max-height: min(100vh - 128px, 900px);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.shell {
|
||||
margin: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
max-height: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.search-bar {
|
||||
padding: 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-bar:focus-within {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.local-search-icon {
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.navigate-icon {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.search-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.search-input {
|
||||
padding: 6px 12px;
|
||||
font-size: inherit;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-input::-webkit-search-cancel-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.search-input {
|
||||
padding: 6px 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
@media (any-pointer: coarse) {
|
||||
.search-actions {
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 769px) {
|
||||
.search-actions.before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.search-actions button {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.search-actions button:not([disabled]):hover,
|
||||
.toggle-layout-button.detailed-list {
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.search-actions button.clear-button:disabled {
|
||||
opacity: 0.37;
|
||||
}
|
||||
|
||||
.search-keyboard-shortcuts {
|
||||
font-size: 0.8rem;
|
||||
opacity: 75%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.search-keyboard-shortcuts span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.search-keyboard-shortcuts {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.search-keyboard-shortcuts kbd {
|
||||
background: rgba(128, 128, 128, 0.1);
|
||||
border-radius: 4px;
|
||||
padding: 3px 6px;
|
||||
min-width: 24px;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
border: 1px solid rgba(128, 128, 128, 0.15);
|
||||
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.results {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.result {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border-radius: 4px;
|
||||
transition: none;
|
||||
line-height: 1rem;
|
||||
border: solid 2px var(--vp-local-search-result-border);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.result > div {
|
||||
margin: 12px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.result > div {
|
||||
margin: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.titles {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
position: relative;
|
||||
z-index: 1001;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.title.main {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
opacity: 0.5;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.title svg {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.result.selected {
|
||||
--vp-local-search-result-bg: var(--vp-local-search-result-selected-bg);
|
||||
border-color: var(--vp-local-search-result-selected-border);
|
||||
}
|
||||
|
||||
.excerpt-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.excerpt {
|
||||
opacity: 50%;
|
||||
pointer-events: none;
|
||||
max-height: 140px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.result.selected .excerpt {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.excerpt :deep(*) {
|
||||
font-size: 0.8rem !important;
|
||||
line-height: 130% !important;
|
||||
}
|
||||
|
||||
.titles :deep(mark),
|
||||
.excerpt :deep(mark) {
|
||||
background-color: var(--vp-local-search-highlight-bg);
|
||||
color: var(--vp-local-search-highlight-text);
|
||||
border-radius: 2px;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.excerpt :deep(.vp-code-group) .tabs {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.excerpt :deep(.vp-code-group) div[class*='language-'] {
|
||||
border-radius: 8px !important;
|
||||
}
|
||||
|
||||
.excerpt-gradient-bottom {
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background: linear-gradient(transparent, var(--vp-local-search-result-bg));
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.excerpt-gradient-top {
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background: linear-gradient(var(--vp-local-search-result-bg), transparent);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.result.selected .titles,
|
||||
.result.selected .title-icon {
|
||||
color: var(--vp-c-brand-1) !important;
|
||||
}
|
||||
|
||||
.no-results {
|
||||
font-size: 0.9rem;
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
svg {
|
||||
flex: none;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,78 @@
|
||||
<script lang="ts" setup generic="T extends DefaultTheme.NavItem">
|
||||
import type { DefaultTheme } from 'vitepress/theme'
|
||||
import VPMenuLink from './VPMenuLink.vue'
|
||||
import VPMenuGroup from './VPMenuGroup.vue'
|
||||
|
||||
defineProps<{
|
||||
items?: T[]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="VPMenu">
|
||||
<div v-if="items" class="items">
|
||||
<template v-for="item in items" :key="JSON.stringify(item)">
|
||||
<VPMenuLink v-if="'link' in item" :item />
|
||||
<component
|
||||
v-else-if="'component' in item"
|
||||
:is="item.component"
|
||||
v-bind="item.props"
|
||||
/>
|
||||
<VPMenuGroup v-else :text="item.text" :items="item.items" />
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPMenu {
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
min-width: 128px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background-color: var(--vp-c-bg-elv);
|
||||
box-shadow: var(--vp-shadow-3);
|
||||
transition: background-color 0.5s;
|
||||
max-height: calc(100vh - var(--vp-nav-height));
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.VPMenu :deep(.group) {
|
||||
margin: 0 -12px;
|
||||
padding: 0 12px 12px;
|
||||
}
|
||||
|
||||
.VPMenu :deep(.group + .group) {
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
padding: 11px 12px 12px;
|
||||
}
|
||||
|
||||
.VPMenu :deep(.group:last-child) {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.VPMenu :deep(.group + .item) {
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
padding: 11px 16px 0;
|
||||
}
|
||||
|
||||
.VPMenu :deep(.item) {
|
||||
padding: 0 16px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.VPMenu :deep(.label) {
|
||||
flex-grow: 1;
|
||||
line-height: 28px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-2);
|
||||
transition: color 0.5s;
|
||||
}
|
||||
|
||||
.VPMenu :deep(.action) {
|
||||
padding-left: 24px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,48 @@
|
||||
<script lang="ts" setup generic="T extends (DefaultTheme.NavItemComponent | DefaultTheme.NavItemChildren | DefaultTheme.NavItemWithLink)">
|
||||
import type { DefaultTheme } from 'vitepress/theme'
|
||||
import VPMenuLink from './VPMenuLink.vue'
|
||||
|
||||
defineProps<{
|
||||
text?: string
|
||||
items: T[]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="VPMenuGroup">
|
||||
<p v-if="text" class="title">{{ text }}</p>
|
||||
|
||||
<template v-for="item in items" :key="JSON.stringify(item)">
|
||||
<VPMenuLink v-if="'link' in item" :item />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPMenuGroup {
|
||||
margin: 12px -12px 0;
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
padding: 12px 12px 0;
|
||||
}
|
||||
|
||||
.VPMenuGroup:first-child {
|
||||
margin-top: 0;
|
||||
border-top: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.VPMenuGroup + .VPMenuGroup {
|
||||
margin-top: 12px;
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.title {
|
||||
padding: 0 12px;
|
||||
line-height: 32px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
white-space: nowrap;
|
||||
transition: color 0.25s;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,63 @@
|
||||
<script lang="ts" setup generic="T extends DefaultTheme.NavItemWithLink">
|
||||
import type { DefaultTheme } from 'vitepress/theme'
|
||||
import { useData } from '../composables/data'
|
||||
import { isActive } from '../../shared'
|
||||
import VPLink from './VPLink.vue'
|
||||
|
||||
defineProps<{
|
||||
item: T
|
||||
}>()
|
||||
|
||||
const { page } = useData()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="VPMenuLink">
|
||||
<VPLink
|
||||
:class="{
|
||||
active: isActive(
|
||||
page.relativePath,
|
||||
item.activeMatch || item.link,
|
||||
!!item.activeMatch
|
||||
)
|
||||
}"
|
||||
:href="item.link"
|
||||
:target="item.target"
|
||||
:rel="item.rel"
|
||||
:no-icon="item.noIcon"
|
||||
>
|
||||
<span v-html="item.text"></span>
|
||||
</VPLink>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPMenuGroup + .VPMenuLink {
|
||||
margin: 12px -12px 0;
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
padding: 12px 12px 0;
|
||||
}
|
||||
|
||||
.link {
|
||||
display: block;
|
||||
border-radius: 6px;
|
||||
padding: 0 12px;
|
||||
line-height: 32px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-1);
|
||||
white-space: nowrap;
|
||||
transition:
|
||||
background-color 0.25s,
|
||||
color 0.25s;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
color: var(--vp-c-brand-1);
|
||||
background-color: var(--vp-c-default-soft);
|
||||
}
|
||||
|
||||
.link.active {
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
</style>
|
@ -0,0 +1,55 @@
|
||||
<script setup lang="ts">
|
||||
import { inBrowser } from 'vitepress'
|
||||
import { computed, provide, watchEffect } from 'vue'
|
||||
import { useData } from '../composables/data'
|
||||
import { navInjectionKey, useNav } from '../composables/nav'
|
||||
import VPNavBar from './VPNavBar.vue'
|
||||
import VPNavScreen from './VPNavScreen.vue'
|
||||
|
||||
const { isScreenOpen, closeScreen, toggleScreen } = useNav()
|
||||
const { frontmatter } = useData()
|
||||
|
||||
const hasNavbar = computed(() => {
|
||||
return frontmatter.value.navbar !== false
|
||||
})
|
||||
|
||||
provide(navInjectionKey, { closeScreen })
|
||||
|
||||
watchEffect(() => {
|
||||
if (inBrowser) {
|
||||
document.documentElement.classList.toggle('hide-nav', !hasNavbar.value)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header v-if="hasNavbar" class="vp-nav">
|
||||
<VPNavBar :is-screen-open="isScreenOpen" @toggle-screen="toggleScreen">
|
||||
<template #nav-bar-title-before><slot name="nav-bar-title-before" /></template>
|
||||
<template #nav-bar-title-after><slot name="nav-bar-title-after" /></template>
|
||||
<template #nav-bar-content-before><slot name="nav-bar-content-before" /></template>
|
||||
<template #nav-bar-content-after><slot name="nav-bar-content-after" /></template>
|
||||
</VPNavBar>
|
||||
<VPNavScreen :open="isScreenOpen">
|
||||
<template #nav-screen-content-before><slot name="nav-screen-content-before" /></template>
|
||||
<template #nav-screen-content-after><slot name="nav-screen-content-after" /></template>
|
||||
</VPNavScreen>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.vp-nav {
|
||||
position: relative;
|
||||
top: var(--vp-layout-top-height, 0px);
|
||||
/*rtl:ignore*/
|
||||
left: 0;
|
||||
z-index: var(--vp-z-index-nav);
|
||||
width: 100%;
|
||||
height: var(--vp-nav-height);
|
||||
/* pointer-events: none; */
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
position: fixed;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,166 @@
|
||||
<script lang="ts" setup>
|
||||
import { useWindowScroll } from '@vueuse/core'
|
||||
import { ref, watchPostEffect } from 'vue'
|
||||
import { useLayout } from '../composables/layout'
|
||||
import VPNavBarAppearance from './VPNavBarAppearance.vue'
|
||||
import VPNavBarExtra from './VPNavBarExtra.vue'
|
||||
import VPNavBarHamburger from './VPNavBarHamburger.vue'
|
||||
import VPNavBarMenu from './VPNavBarMenu.vue'
|
||||
import VPNavBarSearch from './VPNavBarSearch.vue'
|
||||
import VPNavBarSocialLinks from './VPNavBarSocialLinks.vue'
|
||||
import VPNavBarTitle from './VPNavBarTitle.vue'
|
||||
import VPNavBarTranslations from './VPNavBarTranslations.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
isScreenOpen: boolean
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
'toggle-screen': []
|
||||
}>()
|
||||
|
||||
const { y } = useWindowScroll()
|
||||
const { isHome, hasSidebar } = useLayout()
|
||||
|
||||
const classes = ref<Record<string, boolean>>({})
|
||||
|
||||
// TODO: Organize these classes.
|
||||
watchPostEffect(() => {
|
||||
classes.value = {
|
||||
'has-sidebar': hasSidebar.value,
|
||||
'home': isHome.value,
|
||||
'vp-nav-bar__has-scrolled': y.value > 0,
|
||||
'vp-nav-bar__is-screen-open': props.isScreenOpen
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="vp-nav-bar" :class="classes">
|
||||
<div class="vp-nav-bar__wrapper">
|
||||
<div class="vp-nav-bar__container">
|
||||
<div class="vp-nav-bar__title">
|
||||
<VPNavBarTitle>
|
||||
<template #nav-bar-title-before><slot name="nav-bar-title-before" /></template>
|
||||
<template #nav-bar-title-after><slot name="nav-bar-title-after" /></template>
|
||||
</VPNavBarTitle>
|
||||
</div>
|
||||
|
||||
<div class="vp-nav-bar__content">
|
||||
<div class="vp-nav-bar__content-body">
|
||||
<slot name="nav-bar-content-before" />
|
||||
<VPNavBarSearch />
|
||||
<VPNavBarMenu class="menu" />
|
||||
<VPNavBarTranslations class="translations" />
|
||||
<VPNavBarAppearance class="appearance" />
|
||||
<VPNavBarSocialLinks class="social-links" />
|
||||
<VPNavBarExtra class="extra" />
|
||||
<slot name="nav-bar-content-after" />
|
||||
<VPNavBarHamburger :active="isScreenOpen" @click="$emit('toggle-screen')" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.vp-nav-bar {
|
||||
position: relative;
|
||||
/* TODO: Maybe consider a way to customize the border via css var? */
|
||||
border-bottom: 1px solid var(--vp-c-gutter);
|
||||
height: var(--vp-nav-height);
|
||||
white-space: nowrap;
|
||||
transition: background-color 0.25s;
|
||||
|
||||
/* TODO: Why do we need this? */
|
||||
/* pointer-events: none; */
|
||||
}
|
||||
|
||||
.vp-nav-bar__has-scrolled {
|
||||
@media (min-width: 1024px) {
|
||||
background-color: var(--vp-nav-bg-color);
|
||||
}
|
||||
}
|
||||
|
||||
.vp-nav-bar__is-screen-open {
|
||||
background-color: var(--vp-nav-bg-color);
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.vp-nav-bar__wrapper {
|
||||
padding: 0 12px 0 24px;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
padding: 0 32px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.VPNavBar.has-sidebar .wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.vp-nav-bar__container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 0 auto;
|
||||
/* pointer-events: none; */
|
||||
}
|
||||
|
||||
/* .vp-nav-bar__container > .title, */
|
||||
/* .vp-nav-bar__container > .content { */
|
||||
/* pointer-events: none; */
|
||||
/* } */
|
||||
|
||||
/* .vp-nav-bar__container :deep(*) { */
|
||||
/* pointer-events: auto; */
|
||||
/* } */
|
||||
|
||||
.vp-nav-bar__title {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.vp-nav-bar__content {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.vp-nav-bar__content-body {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* @media (max-width: 767px) {
|
||||
.content-body {
|
||||
column-gap: 0.5rem;
|
||||
}
|
||||
} */
|
||||
|
||||
.menu + .translations::before,
|
||||
.menu + .appearance::before,
|
||||
.menu + .social-links::before,
|
||||
.translations + .appearance::before,
|
||||
.appearance + .social-links::before {
|
||||
margin-right: 8px;
|
||||
margin-left: 8px;
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
background-color: var(--vp-c-divider);
|
||||
content: "";
|
||||
}
|
||||
|
||||
.menu + .appearance::before,
|
||||
.translations + .appearance::before {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.appearance + .social-links::before {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.social-links {
|
||||
margin-right: -8px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,32 @@
|
||||
<script lang="ts" setup>
|
||||
import { useData } from '../composables/data'
|
||||
import VPSwitchAppearance from './VPSwitchAppearance.vue'
|
||||
|
||||
const { site } = useData()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="
|
||||
site.appearance &&
|
||||
site.appearance !== 'force-dark' &&
|
||||
site.appearance !== 'force-auto'
|
||||
"
|
||||
class="VPNavBarAppearance"
|
||||
>
|
||||
<VPSwitchAppearance />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPNavBarAppearance {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.VPNavBarAppearance {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,108 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import VPFlyout from './VPFlyout.vue'
|
||||
import VPMenuLink from './VPMenuLink.vue'
|
||||
import VPSwitchAppearance from './VPSwitchAppearance.vue'
|
||||
import VPSocialLinks from './VPSocialLinks.vue'
|
||||
import { useData } from '../composables/data'
|
||||
import { useLangs } from '../composables/langs'
|
||||
|
||||
const { site, theme } = useData()
|
||||
const { localeLinks, currentLang } = useLangs({ correspondingLink: true })
|
||||
|
||||
const hasExtraContent = computed(
|
||||
() =>
|
||||
(localeLinks.value.length && currentLang.value.label) ||
|
||||
site.value.appearance ||
|
||||
theme.value.socialLinks
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VPFlyout
|
||||
v-if="hasExtraContent"
|
||||
class="VPNavBarExtra"
|
||||
label="extra navigation"
|
||||
>
|
||||
<div
|
||||
v-if="localeLinks.length && currentLang.label"
|
||||
class="group translations"
|
||||
>
|
||||
<p class="trans-title">{{ currentLang.label }}</p>
|
||||
|
||||
<template v-for="locale in localeLinks" :key="locale.link">
|
||||
<VPMenuLink :item="locale" />
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="
|
||||
site.appearance &&
|
||||
site.appearance !== 'force-dark' &&
|
||||
site.appearance !== 'force-auto'
|
||||
"
|
||||
class="group"
|
||||
>
|
||||
<div class="item appearance">
|
||||
<p class="label">
|
||||
{{ theme.darkModeSwitchLabel || 'Appearance' }}
|
||||
</p>
|
||||
<div class="appearance-action">
|
||||
<VPSwitchAppearance />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="theme.socialLinks" class="group">
|
||||
<div class="item social-links">
|
||||
<VPSocialLinks class="social-links-list" :links="theme.socialLinks" />
|
||||
</div>
|
||||
</div>
|
||||
</VPFlyout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPNavBarExtra {
|
||||
display: none;
|
||||
margin-right: -12px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.VPNavBarExtra {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.VPNavBarExtra {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.trans-title {
|
||||
padding: 0 24px 0 12px;
|
||||
line-height: 32px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.item.appearance,
|
||||
.item.social-links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.item.appearance {
|
||||
min-width: 176px;
|
||||
}
|
||||
|
||||
.appearance-action {
|
||||
margin-right: -2px;
|
||||
}
|
||||
|
||||
.social-links-list {
|
||||
margin: -4px -8px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,82 @@
|
||||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
active: boolean
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
click: []
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
class="vp-nav-bar-hamburger"
|
||||
:class="{ 'vp-nav-bar-hamburger--is-active': active }"
|
||||
aria-label="mobile navigation"
|
||||
:aria-expanded="active"
|
||||
aria-controls="vp-nav-screen"
|
||||
@click="$emit('click')"
|
||||
>
|
||||
<span class="vp-nav-bar-hamburger__container">
|
||||
<span class="vp-nav-bar-hamburger__top" />
|
||||
<span class="vp-nav-bar-hamburger__middle" />
|
||||
<span class="vp-nav-bar-hamburger__bottom" />
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.vp-nav-bar-hamburger {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 40px;
|
||||
height: var(--vp-nav-height);
|
||||
|
||||
@media (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
:where(.vp-nav-bar-hamburger:hover) {
|
||||
.vp-nav-bar-hamburger__top { top: 0; left: 0; transform: translateX(4px); }
|
||||
.vp-nav-bar-hamburger__middle { top: 6px; left: 0; transform: translateX(0); }
|
||||
.vp-nav-bar-hamburger__bottom { top: 12px; left: 0; transform: translateX(8px); }
|
||||
}
|
||||
|
||||
:where(.vp-nav-bar-hamburger--is-active),
|
||||
:where(.vp-nav-bar-hamburger--is-active:hover) {
|
||||
.vp-nav-bar-hamburger__top,
|
||||
.vp-nav-bar-hamburger__middle,
|
||||
.vp-nav-bar-hamburger__bottom {
|
||||
background-color: var(--vp-c-text-2);
|
||||
transition: top .25s, background-color .25s, transform .25s;
|
||||
}
|
||||
|
||||
.vp-nav-bar-hamburger__top { top: 6px; transform: translateX(0) rotate(225deg); }
|
||||
.vp-nav-bar-hamburger__middle { top: 6px; transform: translateX(16px); }
|
||||
.vp-nav-bar-hamburger__bottom { top: 6px; transform: translateX(0) rotate(135deg); }
|
||||
}
|
||||
|
||||
.vp-nav-bar-hamburger__container {
|
||||
position: relative;
|
||||
width: 16px;
|
||||
height: 14px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.vp-nav-bar-hamburger__top,
|
||||
.vp-nav-bar-hamburger__middle,
|
||||
.vp-nav-bar-hamburger__bottom {
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 2px;
|
||||
background-color: var(--vp-c-text-1);
|
||||
transition: top .25s, background-color .5s, transform .25s;
|
||||
}
|
||||
|
||||
.vp-nav-bar-hamburger__top { top: 0; left: 0; transform: translateX(0); }
|
||||
.vp-nav-bar-hamburger__middle { top: 6px; left: 0; transform: translateX(8px); }
|
||||
.vp-nav-bar-hamburger__bottom { top: 12px; left: 0; transform: translateX(4px); }
|
||||
</style>
|
@ -0,0 +1,40 @@
|
||||
<script lang="ts" setup>
|
||||
import { useData } from '../composables/data'
|
||||
import VPNavBarMenuLink from './VPNavBarMenuLink.vue'
|
||||
import VPNavBarMenuGroup from './VPNavBarMenuGroup.vue'
|
||||
|
||||
const { theme } = useData()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav
|
||||
v-if="theme.nav"
|
||||
aria-labelledby="main-nav-aria-label"
|
||||
class="VPNavBarMenu"
|
||||
>
|
||||
<span id="main-nav-aria-label" class="visually-hidden">
|
||||
Main Navigation
|
||||
</span>
|
||||
<template v-for="item in theme.nav" :key="JSON.stringify(item)">
|
||||
<VPNavBarMenuLink v-if="'link' in item" :item />
|
||||
<component
|
||||
v-else-if="'component' in item"
|
||||
:is="item.component"
|
||||
v-bind="item.props"
|
||||
/>
|
||||
<VPNavBarMenuGroup v-else :item />
|
||||
</template>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPNavBarMenu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.VPNavBarMenu {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,42 @@
|
||||
<script lang="ts" setup>
|
||||
import type { DefaultTheme } from 'vitepress/theme'
|
||||
import { computed } from 'vue'
|
||||
import { useData } from '../composables/data'
|
||||
import { isActive } from '../../shared'
|
||||
import VPFlyout from './VPFlyout.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
item: DefaultTheme.NavItemWithChildren
|
||||
}>()
|
||||
|
||||
const { page } = useData()
|
||||
|
||||
const isChildActive = (navItem: DefaultTheme.NavItem) => {
|
||||
if ('component' in navItem) return false
|
||||
|
||||
if ('link' in navItem) {
|
||||
return isActive(
|
||||
page.value.relativePath,
|
||||
navItem.link,
|
||||
!!props.item.activeMatch
|
||||
)
|
||||
}
|
||||
|
||||
return navItem.items.some(isChildActive)
|
||||
}
|
||||
|
||||
const childrenActive = computed(() => isChildActive(props.item))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VPFlyout
|
||||
:class="{
|
||||
VPNavBarMenuGroup: true,
|
||||
active:
|
||||
isActive(page.relativePath, item.activeMatch, !!item.activeMatch) ||
|
||||
childrenActive
|
||||
}"
|
||||
:button="item.text"
|
||||
:items="item.items"
|
||||
/>
|
||||
</template>
|
@ -0,0 +1,53 @@
|
||||
<script lang="ts" setup>
|
||||
import type { DefaultTheme } from 'vitepress/theme'
|
||||
import { useData } from '../composables/data'
|
||||
import { isActive } from '../../shared'
|
||||
import VPLink from './VPLink.vue'
|
||||
|
||||
defineProps<{
|
||||
item: DefaultTheme.NavItemWithLink
|
||||
}>()
|
||||
|
||||
const { page } = useData()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VPLink
|
||||
:class="{
|
||||
VPNavBarMenuLink: true,
|
||||
active: isActive(
|
||||
page.relativePath,
|
||||
item.activeMatch || item.link,
|
||||
!!item.activeMatch
|
||||
)
|
||||
}"
|
||||
:href="item.link"
|
||||
:target="item.target"
|
||||
:rel="item.rel"
|
||||
:no-icon="item.noIcon"
|
||||
tabindex="0"
|
||||
>
|
||||
<span v-html="item.text"></span>
|
||||
</VPLink>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPNavBarMenuLink {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
line-height: var(--vp-nav-height);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-1);
|
||||
transition: color 0.25s;
|
||||
}
|
||||
|
||||
.VPNavBarMenuLink.active {
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.VPNavBarMenuLink:hover {
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
</style>
|
@ -0,0 +1,160 @@
|
||||
<script lang="ts" setup>
|
||||
import '@docsearch/css'
|
||||
import { onKeyStroke } from '@vueuse/core'
|
||||
import type { DefaultTheme } from 'vitepress/theme'
|
||||
import { defineAsyncComponent, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useData } from '../composables/data'
|
||||
import VPNavBarSearchButton from './VPNavBarSearchButton.vue'
|
||||
|
||||
const VPLocalSearchBox = __VP_LOCAL_SEARCH__
|
||||
? defineAsyncComponent(() => import('./VPLocalSearchBox.vue'))
|
||||
: () => null
|
||||
|
||||
const VPAlgoliaSearchBox = __ALGOLIA__
|
||||
? defineAsyncComponent(() => import('./VPAlgoliaSearchBox.vue'))
|
||||
: () => null
|
||||
|
||||
const { theme } = useData()
|
||||
|
||||
// to avoid loading the docsearch js upfront (which is more than 1/3 of the
|
||||
// payload), we delay initializing it until the user has actually clicked or
|
||||
// hit the hotkey to invoke it.
|
||||
const loaded = ref(false)
|
||||
const actuallyLoaded = ref(false)
|
||||
|
||||
const preconnect = () => {
|
||||
const id = 'VPAlgoliaPreconnect'
|
||||
|
||||
const rIC = window.requestIdleCallback || setTimeout
|
||||
rIC(() => {
|
||||
const preconnect = document.createElement('link')
|
||||
preconnect.id = id
|
||||
preconnect.rel = 'preconnect'
|
||||
preconnect.href = `https://${
|
||||
((theme.value.search?.options as DefaultTheme.AlgoliaSearchOptions) ??
|
||||
theme.value.algolia)!.appId
|
||||
}-dsn.algolia.net`
|
||||
preconnect.crossOrigin = ''
|
||||
document.head.appendChild(preconnect)
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!__ALGOLIA__) {
|
||||
return
|
||||
}
|
||||
|
||||
preconnect()
|
||||
|
||||
const handleSearchHotKey = (event: KeyboardEvent) => {
|
||||
if (
|
||||
(event.key?.toLowerCase() === 'k' && (event.metaKey || event.ctrlKey)) ||
|
||||
(!isEditingContent(event) && event.key === '/')
|
||||
) {
|
||||
event.preventDefault()
|
||||
load()
|
||||
remove()
|
||||
}
|
||||
}
|
||||
|
||||
const remove = () => {
|
||||
window.removeEventListener('keydown', handleSearchHotKey)
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleSearchHotKey)
|
||||
|
||||
onUnmounted(remove)
|
||||
})
|
||||
|
||||
function load() {
|
||||
if (!loaded.value) {
|
||||
loaded.value = true
|
||||
setTimeout(poll, 16)
|
||||
}
|
||||
}
|
||||
|
||||
function poll() {
|
||||
// programmatically open the search box after initialize
|
||||
const e = new Event('keydown') as any
|
||||
|
||||
e.key = 'k'
|
||||
e.metaKey = true
|
||||
|
||||
window.dispatchEvent(e)
|
||||
|
||||
setTimeout(() => {
|
||||
if (!document.querySelector('.DocSearch-Modal')) {
|
||||
poll()
|
||||
}
|
||||
}, 16)
|
||||
}
|
||||
|
||||
function isEditingContent(event: KeyboardEvent): boolean {
|
||||
const element = event.target as HTMLElement
|
||||
const tagName = element.tagName
|
||||
|
||||
return (
|
||||
element.isContentEditable ||
|
||||
tagName === 'INPUT' ||
|
||||
tagName === 'SELECT' ||
|
||||
tagName === 'TEXTAREA'
|
||||
)
|
||||
}
|
||||
|
||||
// Local search
|
||||
|
||||
const showSearch = ref(false)
|
||||
|
||||
if (__VP_LOCAL_SEARCH__) {
|
||||
onKeyStroke('k', (event) => {
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
event.preventDefault()
|
||||
showSearch.value = true
|
||||
}
|
||||
})
|
||||
|
||||
onKeyStroke('/', (event) => {
|
||||
if (!isEditingContent(event)) {
|
||||
event.preventDefault()
|
||||
showSearch.value = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const provider = __ALGOLIA__ ? 'algolia' : __VP_LOCAL_SEARCH__ ? 'local' : ''
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="vp-nav-bar-search">
|
||||
<template v-if="provider === 'local'">
|
||||
<VPLocalSearchBox
|
||||
v-if="showSearch"
|
||||
@close="showSearch = false"
|
||||
/>
|
||||
|
||||
<div id="local-search">
|
||||
<VPNavBarSearchButton @click="showSearch = true" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="provider === 'algolia'">
|
||||
<VPAlgoliaSearchBox
|
||||
v-if="loaded"
|
||||
:algolia="theme.search?.options ?? theme.algolia"
|
||||
@vue:beforeMount="actuallyLoaded = true"
|
||||
/>
|
||||
|
||||
<div v-if="!actuallyLoaded" id="docsearch">
|
||||
<VPNavBarSearchButton @click="load" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.vp-nav-bar-search {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,172 @@
|
||||
<script lang="ts" setup>
|
||||
import type { ButtonTranslations } from '../../../../types/local-search'
|
||||
import { createSearchTranslate } from '../support/translation'
|
||||
|
||||
// button translations
|
||||
const defaultTranslations: { button: ButtonTranslations } = {
|
||||
button: {
|
||||
buttonText: 'Search',
|
||||
buttonAriaLabel: 'Search'
|
||||
}
|
||||
}
|
||||
|
||||
const translate = createSearchTranslate(defaultTranslations)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
class="vp-nav-bar-search-button"
|
||||
:aria-label="translate('button.buttonAriaLabel')"
|
||||
aria-keyshortcuts="/ control+k meta+k"
|
||||
>
|
||||
<!-- <span class="vp-nav-bar-search-button__container"> -->
|
||||
<span class="vp-nav-bar-search-button__icon vpi-search"></span>
|
||||
<span class="DocSearch-Button-Placeholder">{{ translate('button.buttonText') }}</span>
|
||||
<!-- </span> -->
|
||||
<!-- <span class="DocSearch-Button-Keys">
|
||||
<kbd class="DocSearch-Button-Key"></kbd>
|
||||
<kbd class="DocSearch-Button-Key"></kbd>
|
||||
</span> -->
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.vp-nav-bar-search-button {
|
||||
--docsearch-muted-color: var(--docsearch-text-color);
|
||||
--docsearch-searchbox-background: transparent;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 40px;
|
||||
height: var(--vp-nav-height);
|
||||
}
|
||||
|
||||
.vp-nav-bar-search-button__icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Styles for loaded Algolia components. Algolia uses `DocSearch` as a
|
||||
* class name to their elements, such as `DocSearch-Button`.
|
||||
*/
|
||||
[class*='DocSearch'] {
|
||||
--docsearch-actions-height: auto;
|
||||
--docsearch-actions-width: auto;
|
||||
--docsearch-background-color: var(--vp-c-bg-soft);
|
||||
--docsearch-container-background: var(--vp-backdrop-bg-color);
|
||||
--docsearch-focus-color: var(--vp-c-brand-1);
|
||||
--docsearch-footer-background: var(--vp-c-bg-1);
|
||||
--docsearch-highlight-color: var(--vp-c-brand-1);
|
||||
--docsearch-hit-background: var(--vp-c-default-soft);
|
||||
--docsearch-hit-color: var(--vp-c-text-1);
|
||||
--docsearch-hit-highlight-color: var(--vp-c-brand-soft);
|
||||
--docsearch-icon-color: var(--vp-c-text-2);
|
||||
--docsearch-key-background: transparent;
|
||||
--docsearch-key-color: var(--vp-c-text-2);
|
||||
--docsearch-modal-background: var(--vp-c-bg-soft);
|
||||
--docsearch-muted-color: var(--vp-c-text-2);
|
||||
--docsearch-primary-color: var(--vp-c-brand-1);
|
||||
--docsearch-searchbox-focus-background: transparent;
|
||||
--docsearch-secondary-text-color: var(--vp-c-text-2);
|
||||
--docsearch-soft-primary-color: var(--vp-c-brand-soft);
|
||||
--docsearch-subtle-color: var(--vp-c-divider);
|
||||
--docsearch-success-color: var(--vp-c-brand-soft);
|
||||
--docsearch-text-color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.dark [class*='DocSearch-button'] {
|
||||
--docsearch-modal-shadow: none;
|
||||
}
|
||||
|
||||
.DocSearch-Button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0;
|
||||
width: 40px;
|
||||
height: var(--vp-nav-height);
|
||||
background-color: transparent;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.DocSearch-Search-Icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.DocSearch-Button {
|
||||
/* --docsearch-muted-color: var(--docsearch-secondary-text-color);
|
||||
--docsearch-searchbox-background: var(--vp-c-bg-alt); */
|
||||
}
|
||||
|
||||
.DocSearch-Search-Icon {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.DocSearch-Button-Placeholder {
|
||||
/* font-size: 13px; */
|
||||
}
|
||||
}
|
||||
|
||||
.DocSearch-Button-Keys {
|
||||
min-width: auto;
|
||||
margin: 0;
|
||||
padding: 4px 6px;
|
||||
background-color: var(--docsearch-key-background);
|
||||
border: 1px solid var(--docsearch-subtle-color);
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
color: var(--docsearch-key-color);
|
||||
}
|
||||
|
||||
.DocSearch-Button-Keys > * {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.DocSearch-Button-Keys:after {
|
||||
/*rtl:ignore*/
|
||||
direction: ltr;
|
||||
content: 'Ctrl K';
|
||||
}
|
||||
|
||||
.mac .DocSearch-Button-Keys:after {
|
||||
content: '\2318 K';
|
||||
}
|
||||
|
||||
.DocSearch-Clear {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.DocSearch-Commands-Key {
|
||||
border: 1px solid var(--docsearch-subtle-color);
|
||||
border-radius: 4px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.DocSearch-Hit a:focus-visible {
|
||||
outline: 2px solid var(--docsearch-focus-color);
|
||||
}
|
||||
|
||||
.DocSearch-Logo [class^='cls-'] {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.DocSearch-SearchBar + .DocSearch-Footer {
|
||||
border-top-color: transparent;
|
||||
}
|
||||
|
||||
.DocSearch-Title {
|
||||
font-size: revert;
|
||||
line-height: revert;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,27 @@
|
||||
<script lang="ts" setup>
|
||||
import { useData } from '../composables/data'
|
||||
import VPSocialLinks from './VPSocialLinks.vue'
|
||||
|
||||
const { theme } = useData()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VPSocialLinks
|
||||
v-if="theme.socialLinks"
|
||||
class="VPNavBarSocialLinks"
|
||||
:links="theme.socialLinks"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPNavBarSocialLinks {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.VPNavBarSocialLinks {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,74 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useData } from '../composables/data'
|
||||
import { useLangs } from '../composables/langs'
|
||||
import { normalizeLink } from '../support/utils'
|
||||
import VPImage from './VPImage.vue'
|
||||
|
||||
const { site, theme } = useData()
|
||||
const { currentLang } = useLangs()
|
||||
|
||||
const link = computed(() =>
|
||||
typeof theme.value.logoLink === 'string'
|
||||
? theme.value.logoLink
|
||||
: theme.value.logoLink?.link
|
||||
)
|
||||
|
||||
const rel = computed(() =>
|
||||
typeof theme.value.logoLink === 'string'
|
||||
? undefined
|
||||
: theme.value.logoLink?.rel
|
||||
)
|
||||
|
||||
const target = computed(() =>
|
||||
typeof theme.value.logoLink === 'string'
|
||||
? undefined
|
||||
: theme.value.logoLink?.target
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="vp-nav-bar-title">
|
||||
<a
|
||||
class="vp-nav-bar-title__link"
|
||||
:href="link ?? normalizeLink(currentLang.link)"
|
||||
:rel
|
||||
:target
|
||||
>
|
||||
<slot name="nav-bar-title-before" />
|
||||
<VPImage v-if="theme.logo" class="vp-nav-bar-title__logo" :image="theme.logo" />
|
||||
<span v-if="theme.siteTitle" class="vp-nav-bar-title__name" v-html="theme.siteTitle"></span>
|
||||
<span v-else-if="theme.siteTitle === undefined" class="vp-nav-bar-title__name">{{ site.title }}</span>
|
||||
<slot name="nav-bar-title-after" />
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.vp-nav-bar-title__link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: var(--vp-nav-height);
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
transition: opacity 0.25s;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.vp-nav-bar-title__logo {
|
||||
height: var(--vp-nav-logo-height);
|
||||
}
|
||||
|
||||
.vp-nav-bar-title__logo + .vp-nav-bar-title__name {
|
||||
margin-left: 4px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,47 @@
|
||||
<script lang="ts" setup>
|
||||
import VPFlyout from './VPFlyout.vue'
|
||||
import VPMenuLink from './VPMenuLink.vue'
|
||||
import { useData } from '../composables/data'
|
||||
import { useLangs } from '../composables/langs'
|
||||
|
||||
const { theme } = useData()
|
||||
const { localeLinks, currentLang } = useLangs({ correspondingLink: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VPFlyout
|
||||
v-if="localeLinks.length && currentLang.label"
|
||||
class="VPNavBarTranslations"
|
||||
icon="vpi-languages"
|
||||
:label="theme.langMenuLabel || 'Change language'"
|
||||
>
|
||||
<div class="items">
|
||||
<p class="title">{{ currentLang.label }}</p>
|
||||
|
||||
<template v-for="locale in localeLinks" :key="locale.link">
|
||||
<VPMenuLink :item="locale" />
|
||||
</template>
|
||||
</div>
|
||||
</VPFlyout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPNavBarTranslations {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.VPNavBarTranslations {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
padding: 0 24px 0 12px;
|
||||
line-height: 32px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
</style>
|
@ -0,0 +1,88 @@
|
||||
<script setup lang="ts">
|
||||
import { useScrollLock } from '@vueuse/core'
|
||||
import { inBrowser } from 'vitepress'
|
||||
import { ref } from 'vue'
|
||||
import VPNavScreenMenu from './VPNavScreenMenu.vue'
|
||||
import VPNavScreenSocialLinks from './VPNavScreenSocialLinks.vue'
|
||||
import VPNavScreenActions from './VPNavScreenActions.vue'
|
||||
|
||||
defineProps<{
|
||||
open: boolean
|
||||
}>()
|
||||
|
||||
const screen = ref<HTMLElement | null>(null)
|
||||
const isLocked = useScrollLock(inBrowser ? document.body : null)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<transition
|
||||
name="fade"
|
||||
@enter="isLocked = true"
|
||||
@after-leave="isLocked = false"
|
||||
>
|
||||
<div v-if="open" class="vp-nav-screen" ref="screen" id="VPNavScreen">
|
||||
<div class="vp-nav-screen__container">
|
||||
<slot name="nav-screen-content-before" />
|
||||
<VPNavScreenMenu />
|
||||
<VPNavScreenActions />
|
||||
<VPNavScreenSocialLinks />
|
||||
<slot name="nav-screen-content-after" />
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.vp-nav-screen {
|
||||
position: fixed;
|
||||
top: calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px));
|
||||
/*rtl:ignore*/
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
/*rtl:ignore*/
|
||||
left: 0;
|
||||
padding: 0 24px;
|
||||
width: 100%;
|
||||
background-color: var(--vp-nav-screen-bg-color);
|
||||
overflow-y: auto;
|
||||
transition: background-color 0.25s;
|
||||
pointer-events: auto;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.vp-nav-screen.fade-enter-active,
|
||||
.vp-nav-screen.fade-leave-active {
|
||||
transition: opacity 0.25s;
|
||||
|
||||
.vp-nav-screen__container {
|
||||
transition: transform 0.25s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.vp-nav-screen.fade-enter-from,
|
||||
.vp-nav-screen.fade-leave-to {
|
||||
opacity: 0;
|
||||
|
||||
.vp-nav-screen__container {
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
}
|
||||
|
||||
.vp-nav-screen__container {
|
||||
margin: 0 auto;
|
||||
padding: 24px 0 96px;
|
||||
max-width: 288px;
|
||||
}
|
||||
|
||||
.vp-nav-screen-menu + .vp-nav-screen-actions,
|
||||
.vp-nav-screen-menu + .vp-nav-screen-social-links {
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
.vp-nav-screen-actions + .vp-nav-screen-social-links {
|
||||
margin-top: 24px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,125 @@
|
||||
<script setup lang="ts">
|
||||
import { useLangs } from '../composables/langs'
|
||||
import { computed } from 'vue'
|
||||
import { useData } from '../composables/data'
|
||||
import VPSwitchAppearance from './VPSwitchAppearance.vue'
|
||||
|
||||
const { site, theme } = useData()
|
||||
const { localeLinks, currentLang } = useLangs({ correspondingLink: true })
|
||||
|
||||
const state = computed(() => {
|
||||
const appearance = site.value.appearance
|
||||
|
||||
const hasLanguages = localeLinks.value.length && currentLang.value.label
|
||||
const hasTheme = appearance && appearance !== 'force-dark' && appearance !== 'force-auto'
|
||||
const hasAction = hasLanguages || hasTheme
|
||||
|
||||
return {
|
||||
hasAction,
|
||||
hasTheme,
|
||||
hasLanguages
|
||||
}
|
||||
})
|
||||
|
||||
function onLanguageSelect(e: Event) {
|
||||
const select = e.target as HTMLSelectElement
|
||||
window.location.href = select.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="vp-nav-screen-actions">
|
||||
<div v-if="state.hasLanguages" class="vp-nav-screen-actions__item">
|
||||
<!-- TODO: i18n -->
|
||||
<div class="vp-nav-screen-actions__label">
|
||||
Language
|
||||
</div>
|
||||
<div class="vp-nav-screen-actions__value">
|
||||
<div class="vp-nav-screen-actions__select">
|
||||
<select
|
||||
class="vp-nav-screen-actions__select-input"
|
||||
id="vp-nav-screen-actions-language"
|
||||
@change="onLanguageSelect"
|
||||
>
|
||||
<option :value="currentLang.link" selected>
|
||||
{{ currentLang.label }}
|
||||
</option>
|
||||
<option v-for="locale in localeLinks" :key="locale.link" :value="locale.link">
|
||||
{{ locale.text }}
|
||||
</option>
|
||||
</select>
|
||||
<div class="vp-nav-screen-actions__select-icon vpi-chevron-down" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="state.hasTheme" class="vp-nav-screen-actions__item">
|
||||
<div class="vp-nav-screen-actions__label">
|
||||
{{ theme.darkModeSwitchLabel || 'Appearance' }}
|
||||
</div>
|
||||
<div class="vp-nav-screen-actions__value">
|
||||
<VPSwitchAppearance />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.vp-nav-screen-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1px;
|
||||
border-radius: 6px;
|
||||
background-color: var(--vp-c-gutter);
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
.vp-nav-screen-actions__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 8px 0 16px;
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
background-color: var(--vp-c-bg-2);
|
||||
}
|
||||
|
||||
.vp-nav-screen-actions__label {
|
||||
flex-shrink: 0;
|
||||
width: 112px;
|
||||
font-size: 14px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.vp-nav-screen-actions__value {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.vp-nav-screen-actions__select {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.vp-nav-screen-actions__select-input {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
padding: 0 10px;
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
font-size: 14px;
|
||||
color: var(--vp-c-text-1);
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.vp-nav-screen-actions__select-icon {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
z-index: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,40 @@
|
||||
<script lang="ts" setup>
|
||||
import { useData } from '../composables/data'
|
||||
import VPSwitchAppearance from './VPSwitchAppearance.vue'
|
||||
|
||||
const { site, theme } = useData()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="
|
||||
site.appearance &&
|
||||
site.appearance !== 'force-dark' &&
|
||||
site.appearance !== 'force-auto'
|
||||
"
|
||||
class="VPNavScreenAppearance"
|
||||
>
|
||||
<p class="text">
|
||||
{{ theme.darkModeSwitchLabel || 'Appearance' }}
|
||||
</p>
|
||||
<VPSwitchAppearance />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPNavScreenAppearance {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-radius: 8px;
|
||||
padding: 12px 14px 12px 16px;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.text {
|
||||
line-height: 24px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
</style>
|
@ -0,0 +1,26 @@
|
||||
<script lang="ts" setup>
|
||||
import { useData } from '../composables/data'
|
||||
import VPNavScreenMenuLink from './VPNavScreenMenuLink.vue'
|
||||
import VPNavScreenMenuGroup from './VPNavScreenMenuGroup.vue'
|
||||
|
||||
const { theme } = useData()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav v-if="theme.nav" class="vp-nav-screen-menu">
|
||||
<template v-for="item in theme.nav" :key="JSON.stringify(item)">
|
||||
<VPNavScreenMenuLink v-if="'link' in item" :item />
|
||||
<component
|
||||
v-else-if="'component' in item"
|
||||
:is="item.component"
|
||||
v-bind="item.props"
|
||||
screen-menu
|
||||
/>
|
||||
<VPNavScreenMenuGroup
|
||||
v-else
|
||||
:text="item.text || ''"
|
||||
:items="item.items"
|
||||
/>
|
||||
</template>
|
||||
</nav>
|
||||
</template>
|
@ -0,0 +1,110 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import VPNavScreenMenuGroupLink from './VPNavScreenMenuGroupLink.vue'
|
||||
import VPNavScreenMenuGroupSection from './VPNavScreenMenuGroupSection.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
text: string
|
||||
items: any[]
|
||||
}>()
|
||||
|
||||
const isOpen = ref(false)
|
||||
|
||||
const groupId = computed(
|
||||
() => `NavScreenGroup-${props.text.replace(' ', '-').toLowerCase()}`
|
||||
)
|
||||
|
||||
function toggle() {
|
||||
isOpen.value = !isOpen.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="vp-nav-screen-menu-group" :class="{ 'vp-nav-screen-menu-group--is-open': isOpen }">
|
||||
<button
|
||||
class="vp-nav-screen-menu-group__button"
|
||||
:aria-controls="groupId"
|
||||
:aria-expanded="isOpen"
|
||||
@click="toggle"
|
||||
>
|
||||
<span class="vp-nav-screen-menu-group__button-text" v-html="text" />
|
||||
<span class="vp-nav-screen-menu-group__button-icon vpi-plus" />
|
||||
</button>
|
||||
|
||||
<div :id="groupId" class="vp-nav-screen-menu-group__items">
|
||||
<template v-for="item in items" :key="JSON.stringify(item)">
|
||||
<div v-if="'link' in item" class="vp-nav-screen-menu-group__item">
|
||||
<VPNavScreenMenuGroupLink :item />
|
||||
</div>
|
||||
|
||||
<div v-else-if="'component' in item" class="vp-nav-screen-menu-group__item">
|
||||
<component :is="item.component" v-bind="item.props" screen-menu />
|
||||
</div>
|
||||
|
||||
<div v-else class="vp-nav-screen-menu-group__nest">
|
||||
<VPNavScreenMenuGroupSection :text="item.text" :items="item.items" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.vp-nav-screen-menu-group {
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
height: 48px;
|
||||
overflow: hidden;
|
||||
transition: border-color 0.5s;
|
||||
|
||||
&:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.vp-nav-screen-menu-group--is-open {
|
||||
padding-bottom: 10px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.vp-nav-screen-menu-group--is-open {
|
||||
.vp-nav-screen-menu-group__button {
|
||||
padding-bottom: 4px;
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.vp-nav-screen-menu-group__button-icon {
|
||||
/*rtl:ignore*/
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.vp-nav-screen-menu-group__items {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.vp-nav-screen-menu-group__button {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 4px 11px 0;
|
||||
width: 100%;
|
||||
line-height: 24px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-1);
|
||||
transition: color 0.25s;
|
||||
|
||||
&:hover {
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
}
|
||||
|
||||
.vp-nav-screen-menu-group__button-icon {
|
||||
transition: transform 0.25s;
|
||||
}
|
||||
|
||||
.vp-nav-screen-menu-group__nest + .vp-nav-screen-menu-group__nest,
|
||||
.vp-nav-screen-menu-group__nest + .vp-nav-screen-menu-group__item {
|
||||
padding-top: 4px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,41 @@
|
||||
<script lang="ts" setup>
|
||||
import type { DefaultTheme } from 'vitepress/theme'
|
||||
import { inject } from 'vue'
|
||||
import { navInjectionKey } from '../composables/nav'
|
||||
import VPLink from './VPLink.vue'
|
||||
|
||||
defineProps<{
|
||||
item: DefaultTheme.NavItemWithLink
|
||||
}>()
|
||||
|
||||
const { closeScreen } = inject(navInjectionKey)!
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VPLink
|
||||
class="vp-nav-screen-menu-group-link"
|
||||
:href="item.link"
|
||||
:target="item.target"
|
||||
:rel="item.rel"
|
||||
:no-icon="item.noIcon"
|
||||
@click="closeScreen"
|
||||
>
|
||||
<span v-html="item.text" />
|
||||
</VPLink>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.vp-nav-screen-menu-group-link {
|
||||
display: block;
|
||||
margin-left: 12px;
|
||||
line-height: 32px;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: var(--vp-c-text-1);
|
||||
transition: color 0.25s;
|
||||
|
||||
&:hover {
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,30 @@
|
||||
<script lang="ts" setup>
|
||||
import type { DefaultTheme } from 'vitepress/theme'
|
||||
import VPNavScreenMenuGroupLink from './VPNavScreenMenuGroupLink.vue'
|
||||
|
||||
defineProps<{
|
||||
text?: string
|
||||
items: DefaultTheme.NavItemWithLink[]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="vp-nav-screen-menu-group-section">
|
||||
<p v-if="text" class="vp-nav-screen-menu-group-section__title">{{ text }}</p>
|
||||
<VPNavScreenMenuGroupLink v-for="item in items" :key="item.text" :item />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.vp-nav-screen-menu-group-section {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.vp-nav-screen-menu-group-section__title {
|
||||
line-height: 32px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-2);
|
||||
transition: color 0.25s;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,44 @@
|
||||
<script lang="ts" setup>
|
||||
import type { DefaultTheme } from 'vitepress/theme'
|
||||
import { inject } from 'vue'
|
||||
import { navInjectionKey } from '../composables/nav'
|
||||
import VPLink from './VPLink.vue'
|
||||
|
||||
defineProps<{
|
||||
item: DefaultTheme.NavItemWithLink
|
||||
}>()
|
||||
|
||||
const { closeScreen } = inject(navInjectionKey)!
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VPLink
|
||||
class="vp-nav-screen-menu-link"
|
||||
:href="item.link"
|
||||
:target="item.target"
|
||||
:rel="item.rel"
|
||||
:no-icon="item.noIcon"
|
||||
@click="closeScreen"
|
||||
>
|
||||
<span v-html="item.text" />
|
||||
</VPLink>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.vp-nav-screen-menu-link {
|
||||
display: block;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
padding: 12px 0 11px;
|
||||
line-height: 24px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-1);
|
||||
transition:
|
||||
color 0.25s,
|
||||
border-color 0.25s;
|
||||
|
||||
&:hover {
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,14 @@
|
||||
<script lang="ts" setup>
|
||||
import { useData } from '../composables/data'
|
||||
import VPSocialLinks from './VPSocialLinks.vue'
|
||||
|
||||
const { theme } = useData()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VPSocialLinks
|
||||
v-if="theme.socialLinks"
|
||||
class="vp-nav-screen-social-links"
|
||||
:links="theme.socialLinks"
|
||||
/>
|
||||
</template>
|
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<div class="VPPage">
|
||||
<slot name="page-top" />
|
||||
<Content />
|
||||
<slot name="page-bottom" />
|
||||
</div>
|
||||
</template>
|
@ -0,0 +1,136 @@
|
||||
<script lang="ts" setup>
|
||||
import { useScrollLock } from '@vueuse/core'
|
||||
import { inBrowser } from 'vitepress'
|
||||
import { ref, watch } from 'vue'
|
||||
import { useLayout } from '../composables/layout'
|
||||
import VPSidebarGroup from './VPSidebarGroup.vue'
|
||||
|
||||
const { sidebarGroups, hasSidebar } = useLayout()
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean
|
||||
}>()
|
||||
|
||||
// a11y: focus Nav element when menu has opened
|
||||
const navEl = ref<HTMLElement | null>(null)
|
||||
const isLocked = useScrollLock(inBrowser ? document.body : null)
|
||||
|
||||
watch(
|
||||
[props, navEl],
|
||||
() => {
|
||||
if (props.open) {
|
||||
isLocked.value = true
|
||||
navEl.value?.focus()
|
||||
} else isLocked.value = false
|
||||
},
|
||||
{ immediate: true, flush: 'post' }
|
||||
)
|
||||
|
||||
const key = ref(0)
|
||||
|
||||
watch(
|
||||
sidebarGroups,
|
||||
() => {
|
||||
key.value += 1
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside
|
||||
v-if="hasSidebar"
|
||||
class="VPSidebar"
|
||||
:class="{ open }"
|
||||
ref="navEl"
|
||||
@click.stop
|
||||
>
|
||||
<div class="curtain" />
|
||||
|
||||
<nav
|
||||
class="nav"
|
||||
id="VPSidebarNav"
|
||||
aria-labelledby="sidebar-aria-label"
|
||||
tabindex="-1"
|
||||
>
|
||||
<span class="visually-hidden" id="sidebar-aria-label">
|
||||
Sidebar Navigation
|
||||
</span>
|
||||
|
||||
<slot name="sidebar-nav-before" />
|
||||
<VPSidebarGroup :items="sidebarGroups" :key />
|
||||
<slot name="sidebar-nav-after" />
|
||||
</nav>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPSidebar {
|
||||
position: fixed;
|
||||
top: var(--vp-layout-top-height, 0px);
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: var(--vp-z-index-sidebar);
|
||||
padding: 32px 32px 96px;
|
||||
width: calc(100vw - 64px);
|
||||
max-width: 320px;
|
||||
background-color: var(--vp-sidebar-bg-color);
|
||||
opacity: 0;
|
||||
box-shadow: var(--vp-c-shadow-3);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
transform: translateX(-100%);
|
||||
transition: opacity 0.5s, transform 0.25s ease;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.VPSidebar.open {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateX(0);
|
||||
transition: opacity 0.25s,
|
||||
transform 0.5s cubic-bezier(0.19, 1, 0.22, 1);
|
||||
}
|
||||
|
||||
.dark .VPSidebar {
|
||||
box-shadow: var(--vp-shadow-1);
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.VPSidebar {
|
||||
padding-top: var(--vp-nav-height);
|
||||
width: var(--vp-sidebar-width);
|
||||
max-width: 100%;
|
||||
background-color: var(--vp-sidebar-bg-color);
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
box-shadow: none;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1440px) {
|
||||
.VPSidebar {
|
||||
padding-left: max(32px, calc((100vw - (var(--vp-layout-max-width) - 64px)) / 2));
|
||||
width: calc((100vw - (var(--vp-layout-max-width) - 64px)) / 2 + var(--vp-sidebar-width) - 32px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.curtain {
|
||||
position: sticky;
|
||||
top: -64px;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
margin-top: calc(var(--vp-nav-height) * -1);
|
||||
margin-right: -32px;
|
||||
margin-left: -32px;
|
||||
height: var(--vp-nav-height);
|
||||
background-color: var(--vp-sidebar-bg-color);
|
||||
}
|
||||
}
|
||||
|
||||
.nav {
|
||||
outline: 0;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,56 @@
|
||||
<script setup lang="ts">
|
||||
import type { DefaultTheme } from 'vitepress/theme'
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import VPSidebarItem from './VPSidebarItem.vue'
|
||||
|
||||
defineProps<{
|
||||
items: DefaultTheme.SidebarItem[]
|
||||
}>()
|
||||
|
||||
const disableTransition = ref(true)
|
||||
|
||||
let timer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
onMounted(() => {
|
||||
timer = setTimeout(() => {
|
||||
timer = null
|
||||
disableTransition.value = false
|
||||
}, 300)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (timer != null) {
|
||||
clearTimeout(timer)
|
||||
timer = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-for="item in items"
|
||||
:key="item.text"
|
||||
class="group"
|
||||
:class="{ 'no-transition': disableTransition }"
|
||||
>
|
||||
<VPSidebarItem :item :depth="0" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.no-transition :deep(.caret-icon) {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.group + .group {
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.group {
|
||||
padding-top: 10px;
|
||||
width: calc(var(--vp-sidebar-width) - 64px);
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,251 @@
|
||||
<script setup lang="ts">
|
||||
import type { DefaultTheme } from 'vitepress/theme'
|
||||
import { computed } from 'vue'
|
||||
import { useSidebarItemControl } from '../composables/sidebar'
|
||||
import VPLink from './VPLink.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
item: DefaultTheme.SidebarItem
|
||||
depth: number
|
||||
}>()
|
||||
|
||||
const {
|
||||
collapsed,
|
||||
collapsible,
|
||||
isLink,
|
||||
isActiveLink,
|
||||
hasActiveLink,
|
||||
hasChildren,
|
||||
toggle
|
||||
} = useSidebarItemControl(computed(() => props.item))
|
||||
|
||||
const sectionTag = computed(() => (hasChildren.value ? 'section' : `div`))
|
||||
|
||||
const linkTag = computed(() => (isLink.value ? 'a' : 'div'))
|
||||
|
||||
const textTag = computed(() => {
|
||||
return !hasChildren.value
|
||||
? 'p'
|
||||
: props.depth + 2 === 7
|
||||
? 'p'
|
||||
: `h${props.depth + 2}`
|
||||
})
|
||||
|
||||
const itemRole = computed(() => (isLink.value ? undefined : 'button'))
|
||||
|
||||
const classes = computed(() => [
|
||||
[`level-${props.depth}`],
|
||||
{ collapsible: collapsible.value },
|
||||
{ collapsed: collapsed.value },
|
||||
{ 'is-link': isLink.value },
|
||||
{ 'is-active': isActiveLink.value },
|
||||
{ 'has-active': hasActiveLink.value }
|
||||
])
|
||||
|
||||
function onItemInteraction(e: MouseEvent | Event) {
|
||||
if ('key' in e && e.key !== 'Enter') {
|
||||
return
|
||||
}
|
||||
!props.item.link && toggle()
|
||||
}
|
||||
|
||||
function onCaretClick() {
|
||||
props.item.link && toggle()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="sectionTag" class="VPSidebarItem" :class="classes">
|
||||
<div
|
||||
v-if="item.text"
|
||||
class="item"
|
||||
:role="itemRole"
|
||||
v-on="
|
||||
item.items
|
||||
? { click: onItemInteraction, keydown: onItemInteraction }
|
||||
: {}
|
||||
"
|
||||
:tabindex="item.items && 0"
|
||||
>
|
||||
<div class="indicator" />
|
||||
|
||||
<VPLink
|
||||
v-if="item.link"
|
||||
:tag="linkTag"
|
||||
class="link"
|
||||
:href="item.link"
|
||||
:rel="item.rel"
|
||||
:target="item.target"
|
||||
>
|
||||
<component :is="textTag" class="text" v-html="item.text" />
|
||||
</VPLink>
|
||||
<component v-else :is="textTag" class="text" v-html="item.text" />
|
||||
|
||||
<div
|
||||
v-if="item.collapsed != null && item.items && item.items.length"
|
||||
class="caret"
|
||||
role="button"
|
||||
aria-label="toggle section"
|
||||
@click="onCaretClick"
|
||||
@keydown.enter="onCaretClick"
|
||||
tabindex="0"
|
||||
>
|
||||
<span class="vpi-chevron-right caret-icon" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="item.items && item.items.length" class="items">
|
||||
<template v-if="depth < 5">
|
||||
<VPSidebarItem
|
||||
v-for="i in item.items"
|
||||
:key="i.text"
|
||||
:item="i"
|
||||
:depth="depth + 1"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPSidebarItem.level-0 {
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.VPSidebarItem.collapsed.level-0 {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.VPSidebarItem.collapsible > .item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.indicator {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
bottom: 6px;
|
||||
left: -17px;
|
||||
width: 2px;
|
||||
border-radius: 2px;
|
||||
transition: background-color 0.25s;
|
||||
}
|
||||
|
||||
.VPSidebarItem.level-2.is-active > .item > .indicator,
|
||||
.VPSidebarItem.level-3.is-active > .item > .indicator,
|
||||
.VPSidebarItem.level-4.is-active > .item > .indicator,
|
||||
.VPSidebarItem.level-5.is-active > .item > .indicator {
|
||||
background-color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.text {
|
||||
flex-grow: 1;
|
||||
padding: 4px 0;
|
||||
line-height: 24px;
|
||||
font-size: 14px;
|
||||
transition: color 0.25s;
|
||||
}
|
||||
|
||||
.VPSidebarItem.level-0 .text {
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.VPSidebarItem.level-1 .text,
|
||||
.VPSidebarItem.level-2 .text,
|
||||
.VPSidebarItem.level-3 .text,
|
||||
.VPSidebarItem.level-4 .text,
|
||||
.VPSidebarItem.level-5 .text {
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.VPSidebarItem.level-0.is-link > .item > .link:hover .text,
|
||||
.VPSidebarItem.level-1.is-link > .item > .link:hover .text,
|
||||
.VPSidebarItem.level-2.is-link > .item > .link:hover .text,
|
||||
.VPSidebarItem.level-3.is-link > .item > .link:hover .text,
|
||||
.VPSidebarItem.level-4.is-link > .item > .link:hover .text,
|
||||
.VPSidebarItem.level-5.is-link > .item > .link:hover .text {
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.VPSidebarItem.level-0.has-active > .item > .text,
|
||||
.VPSidebarItem.level-1.has-active > .item > .text,
|
||||
.VPSidebarItem.level-2.has-active > .item > .text,
|
||||
.VPSidebarItem.level-3.has-active > .item > .text,
|
||||
.VPSidebarItem.level-4.has-active > .item > .text,
|
||||
.VPSidebarItem.level-5.has-active > .item > .text,
|
||||
.VPSidebarItem.level-0.has-active > .item > .link > .text,
|
||||
.VPSidebarItem.level-1.has-active > .item > .link > .text,
|
||||
.VPSidebarItem.level-2.has-active > .item > .link > .text,
|
||||
.VPSidebarItem.level-3.has-active > .item > .link > .text,
|
||||
.VPSidebarItem.level-4.has-active > .item > .link > .text,
|
||||
.VPSidebarItem.level-5.has-active > .item > .link > .text {
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.VPSidebarItem.level-0.is-active > .item .link > .text,
|
||||
.VPSidebarItem.level-1.is-active > .item .link > .text,
|
||||
.VPSidebarItem.level-2.is-active > .item .link > .text,
|
||||
.VPSidebarItem.level-3.is-active > .item .link > .text,
|
||||
.VPSidebarItem.level-4.is-active > .item .link > .text,
|
||||
.VPSidebarItem.level-5.is-active > .item .link > .text {
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.caret {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-right: -7px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
color: var(--vp-c-text-3);
|
||||
cursor: pointer;
|
||||
transition: color 0.25s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.item:hover .caret {
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.item:hover .caret:hover {
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.caret-icon {
|
||||
font-size: 18px;
|
||||
/*rtl:ignore*/
|
||||
transform: rotate(90deg);
|
||||
transition: transform 0.25s;
|
||||
}
|
||||
|
||||
.VPSidebarItem.collapsed .caret-icon {
|
||||
transform: rotate(0)/*rtl:rotate(180deg)*/;
|
||||
}
|
||||
|
||||
.VPSidebarItem.level-1 .items,
|
||||
.VPSidebarItem.level-2 .items,
|
||||
.VPSidebarItem.level-3 .items,
|
||||
.VPSidebarItem.level-4 .items,
|
||||
.VPSidebarItem.level-5 .items {
|
||||
border-left: 1px solid var(--vp-c-divider);
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.VPSidebarItem.collapsed .items {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,70 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { useRoute } from 'vitepress'
|
||||
import { useData } from '../composables/data'
|
||||
|
||||
const { theme } = useData()
|
||||
const route = useRoute()
|
||||
const backToTop = ref()
|
||||
|
||||
watch(() => route.path, () => backToTop.value.focus())
|
||||
|
||||
function focusOnTargetAnchor({ target }: Event) {
|
||||
const el = document.getElementById(
|
||||
decodeURIComponent((target as HTMLAnchorElement).hash).slice(1)
|
||||
)
|
||||
|
||||
if (el) {
|
||||
const removeTabIndex = () => {
|
||||
el.removeAttribute('tabindex')
|
||||
el.removeEventListener('blur', removeTabIndex)
|
||||
}
|
||||
|
||||
el.setAttribute('tabindex', '-1')
|
||||
el.addEventListener('blur', removeTabIndex)
|
||||
el.focus()
|
||||
window.scrollTo(0, 0)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span ref="backToTop" tabindex="-1" />
|
||||
<a
|
||||
href="#VPContent"
|
||||
class="VPSkipLink visually-hidden"
|
||||
@click="focusOnTargetAnchor"
|
||||
>
|
||||
{{ theme.skipToContentLabel || 'Skip to content' }}
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPSkipLink {
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
padding: 8px 16px;
|
||||
z-index: 999;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
color: var(--vp-c-brand-1);
|
||||
box-shadow: var(--vp-shadow-3);
|
||||
background-color: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
.VPSkipLink:focus {
|
||||
height: auto;
|
||||
width: auto;
|
||||
clip: auto;
|
||||
clip-path: none;
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.VPSkipLink {
|
||||
top: 14px;
|
||||
left: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,76 @@
|
||||
<script lang="ts" setup>
|
||||
import type { DefaultTheme } from 'vitepress/theme'
|
||||
import { computed, nextTick, onMounted, ref, useSSRContext } from 'vue'
|
||||
import type { SSGContext } from '../../shared'
|
||||
|
||||
const props = defineProps<{
|
||||
icon: DefaultTheme.SocialLinkIcon
|
||||
link: string
|
||||
ariaLabel?: string
|
||||
me: boolean
|
||||
}>()
|
||||
|
||||
const el = ref<HTMLAnchorElement>()
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick()
|
||||
const span = el.value?.children[0]
|
||||
if (
|
||||
span instanceof HTMLElement &&
|
||||
span.className.startsWith('vpi-social-') &&
|
||||
(getComputedStyle(span).maskImage ||
|
||||
getComputedStyle(span).webkitMaskImage) === 'none'
|
||||
) {
|
||||
span.style.setProperty(
|
||||
'--icon',
|
||||
`url('https://api.iconify.design/simple-icons/${props.icon}.svg')`
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const svg = computed(() => {
|
||||
if (typeof props.icon === 'object') return props.icon.svg
|
||||
return `<span class="vpi-social-${props.icon}"></span>`
|
||||
})
|
||||
|
||||
if (import.meta.env.SSR) {
|
||||
typeof props.icon === 'string' &&
|
||||
useSSRContext<SSGContext>()?.vpSocialIcons.add(props.icon)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a
|
||||
ref="el"
|
||||
class="vp-social-link no-icon"
|
||||
:href="link"
|
||||
:aria-label="ariaLabel ?? (typeof icon === 'string' ? icon : '')"
|
||||
target="_blank"
|
||||
:rel="me ? 'me noopener' : 'noopener'"
|
||||
v-html="svg"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.vp-social-link {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
color: var(--vp-c-text-2);
|
||||
transition: color 0.25s;
|
||||
|
||||
&:hover {
|
||||
color: var(--vp-c-text-1);
|
||||
transition: color 0.25s;
|
||||
}
|
||||
}
|
||||
|
||||
.vp-social-link svg,
|
||||
.vp-social-link [class^="vpi-social-"] {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
fill: currentColor;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,31 @@
|
||||
<script lang="ts" setup>
|
||||
import type { DefaultTheme } from 'vitepress/theme'
|
||||
import VPSocialLink from './VPSocialLink.vue'
|
||||
|
||||
withDefaults(defineProps<{
|
||||
links: DefaultTheme.SocialLink[]
|
||||
me?: boolean
|
||||
}>(), {
|
||||
me: true
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="vp-social-links">
|
||||
<VPSocialLink
|
||||
v-for="{ link, icon, ariaLabel } in links"
|
||||
:key="link"
|
||||
:icon
|
||||
:link
|
||||
:ariaLabel
|
||||
:me
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.vp-social-links {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
import type { GridSize } from '../composables/sponsor-grid'
|
||||
import type { Sponsor } from './VPSponsorsGrid.vue'
|
||||
import { computed } from 'vue'
|
||||
import VPSponsorsGrid from './VPSponsorsGrid.vue'
|
||||
|
||||
export interface Sponsors {
|
||||
tier?: string
|
||||
size?: GridSize
|
||||
items: Sponsor[]
|
||||
}
|
||||
interface Props {
|
||||
mode?: 'normal' | 'aside'
|
||||
tier?: string
|
||||
size?: GridSize
|
||||
data: Sponsors[] | Sponsor[]
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
mode: 'normal'
|
||||
})
|
||||
|
||||
const sponsors = computed(() => {
|
||||
const isSponsors = props.data.some((s) => {
|
||||
return 'items' in s
|
||||
})
|
||||
|
||||
if (isSponsors) {
|
||||
return props.data as Sponsors[]
|
||||
}
|
||||
|
||||
return [
|
||||
{ tier: props.tier, size: props.size, items: props.data as Sponsor[] }
|
||||
]
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="VPSponsors vp-sponsor" :class="[mode]">
|
||||
<section
|
||||
v-for="(sponsor, index) in sponsors"
|
||||
:key="index"
|
||||
class="vp-sponsor-section"
|
||||
>
|
||||
<h3 v-if="sponsor.tier" class="vp-sponsor-tier">{{ sponsor.tier }}</h3>
|
||||
<VPSponsorsGrid :size="sponsor.size" :data="sponsor.items" />
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
@ -0,0 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
import type { GridSize } from '../composables/sponsor-grid'
|
||||
import { ref } from 'vue'
|
||||
import { useSponsorsGrid } from '../composables/sponsor-grid'
|
||||
|
||||
export interface Sponsor {
|
||||
name: string
|
||||
img: string
|
||||
url: string
|
||||
}
|
||||
interface Props {
|
||||
size?: GridSize
|
||||
data: Sponsor[]
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
size: 'medium'
|
||||
})
|
||||
|
||||
const el = ref(null)
|
||||
|
||||
useSponsorsGrid({ el, size: props.size })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="VPSponsorsGrid vp-sponsor-grid" :class="[size]" ref="el">
|
||||
<div
|
||||
v-for="sponsor in data"
|
||||
:key="sponsor.name"
|
||||
class="vp-sponsor-grid-item"
|
||||
>
|
||||
<a
|
||||
class="vp-sponsor-grid-link"
|
||||
:href="sponsor.url"
|
||||
target="_blank"
|
||||
rel="sponsored noopener"
|
||||
>
|
||||
<article class="vp-sponsor-grid-box">
|
||||
<img
|
||||
class="vp-sponsor-grid-image"
|
||||
:src="sponsor.img"
|
||||
:alt="sponsor.name"
|
||||
/>
|
||||
</article>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<button class="VPSwitch" type="button" role="switch">
|
||||
<span class="check">
|
||||
<span class="icon" v-if="$slots.default">
|
||||
<slot />
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPSwitch {
|
||||
position: relative;
|
||||
border-radius: 11px;
|
||||
display: block;
|
||||
width: 40px;
|
||||
height: 22px;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid var(--vp-input-border-color);
|
||||
background-color: var(--vp-input-switch-bg-color);
|
||||
transition: border-color 0.25s !important;
|
||||
}
|
||||
|
||||
.VPSwitch:hover {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.check {
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
/*rtl:ignore*/
|
||||
left: 1px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--vp-c-neutral-inverse);
|
||||
box-shadow: var(--vp-shadow-1);
|
||||
transition: transform 0.25s !important;
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.icon :deep([class^='vpi-']) {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.dark .icon :deep([class^='vpi-']) {
|
||||
color: var(--vp-c-text-1);
|
||||
transition: opacity 0.25s !important;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,71 @@
|
||||
<script lang="ts" setup>
|
||||
import { inject } from 'vue'
|
||||
import { useData } from '../composables/data'
|
||||
|
||||
const { isDark, theme } = useData()
|
||||
|
||||
const toggleAppearance = inject('toggle-appearance', () => {
|
||||
isDark.value = !isDark.value
|
||||
})
|
||||
|
||||
function onClick(theme: 'light' | 'dark') {
|
||||
if ((theme === 'light' && isDark.value) || (theme === 'dark' && !isDark.value)) {
|
||||
toggleAppearance()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="vp-switch-appearance">
|
||||
<button
|
||||
class="vp-switch-appearance__button"
|
||||
:class="{ 'vp-switch-appearance__button--is-active': !isDark }"
|
||||
:title="theme.lightModeSwitchTitle"
|
||||
@click="onClick('light')"
|
||||
>
|
||||
<span class="vp-switch-appearance__icon vpi-sun" />
|
||||
</button>
|
||||
<button
|
||||
class="vp-switch-appearance__button"
|
||||
:class="{ 'vp-switch-appearance__button--is-active': isDark }"
|
||||
:title="theme.darkModeSwitchTitle"
|
||||
@click="onClick('dark')"
|
||||
>
|
||||
<span class="vp-switch-appearance__icon vpi-moon" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.vp-switch-appearance {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
padding: 2px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.vp-switch-appearance__button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 3px;
|
||||
height: 26px;
|
||||
color: var(--vp-c-text-2);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.vp-switch-appearance__button--is-active {
|
||||
border-color: var(--vp-c-divider);
|
||||
color: var(--vp-c-text-1);
|
||||
background-color: var(--vp-c-bg-3);
|
||||
}
|
||||
|
||||
.vp-switch-appearance__icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
import type { DefaultTheme } from 'vitepress/theme'
|
||||
import { computed } from 'vue'
|
||||
import VPTeamMembersItem from './VPTeamMembersItem.vue'
|
||||
|
||||
interface Props {
|
||||
size?: 'small' | 'medium'
|
||||
members: DefaultTheme.TeamMember[]
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
size: 'medium'
|
||||
})
|
||||
|
||||
const classes = computed(() => [props.size, `count-${props.members.length}`])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="VPTeamMembers" :class="classes">
|
||||
<div class="container">
|
||||
<div v-for="member in members" :key="member.name" class="item">
|
||||
<VPTeamMembersItem :size :member />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPTeamMembers.small .container {
|
||||
grid-template-columns: repeat(auto-fit, minmax(224px, 1fr));
|
||||
}
|
||||
|
||||
.VPTeamMembers.small.count-1 .container {
|
||||
max-width: 276px;
|
||||
}
|
||||
.VPTeamMembers.small.count-2 .container {
|
||||
max-width: calc(276px * 2 + 24px);
|
||||
}
|
||||
.VPTeamMembers.small.count-3 .container {
|
||||
max-width: calc(276px * 3 + 24px * 2);
|
||||
}
|
||||
|
||||
.VPTeamMembers.medium .container {
|
||||
grid-template-columns: repeat(auto-fit, minmax(256px, 1fr));
|
||||
}
|
||||
|
||||
@media (min-width: 375px) {
|
||||
.VPTeamMembers.medium .container {
|
||||
grid-template-columns: repeat(auto-fit, minmax(288px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.VPTeamMembers.medium.count-1 .container {
|
||||
max-width: 368px;
|
||||
}
|
||||
.VPTeamMembers.medium.count-2 .container {
|
||||
max-width: calc(368px * 2 + 24px);
|
||||
}
|
||||
|
||||
.container {
|
||||
display: grid;
|
||||
gap: 24px;
|
||||
margin: 0 auto;
|
||||
max-width: 1152px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,225 @@
|
||||
<script setup lang="ts">
|
||||
import type { DefaultTheme } from 'vitepress/theme'
|
||||
import VPLink from './VPLink.vue'
|
||||
import VPSocialLinks from './VPSocialLinks.vue'
|
||||
|
||||
interface Props {
|
||||
size?: 'small' | 'medium'
|
||||
member: DefaultTheme.TeamMember
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
size: 'medium'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<article class="VPTeamMembersItem" :class="[size]">
|
||||
<div class="profile">
|
||||
<figure class="avatar">
|
||||
<img class="avatar-img" :src="member.avatar" :alt="member.name" />
|
||||
</figure>
|
||||
<div class="data">
|
||||
<h1 class="name">
|
||||
{{ member.name }}
|
||||
</h1>
|
||||
<p v-if="member.title || member.org" class="affiliation">
|
||||
<span v-if="member.title" class="title">
|
||||
{{ member.title }}
|
||||
</span>
|
||||
<span v-if="member.title && member.org" class="at"> @ </span>
|
||||
<VPLink
|
||||
v-if="member.org"
|
||||
class="org"
|
||||
:class="{ link: member.orgLink }"
|
||||
:href="member.orgLink"
|
||||
no-icon
|
||||
>
|
||||
{{ member.org }}
|
||||
</VPLink>
|
||||
</p>
|
||||
<p v-if="member.desc" class="desc" v-html="member.desc" />
|
||||
<div v-if="member.links" class="links">
|
||||
<VPSocialLinks :links="member.links" :me="false" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="member.sponsor" class="sp">
|
||||
<VPLink class="sp-link" :href="member.sponsor" no-icon>
|
||||
<span class="vpi-heart sp-icon" /> {{ member.actionText || 'Sponsor' }}
|
||||
</VPLink>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPTeamMembersItem {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
border-radius: 12px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.VPTeamMembersItem.small .profile {
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.VPTeamMembersItem.small .data {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.VPTeamMembersItem.small .avatar {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.VPTeamMembersItem.small .name {
|
||||
line-height: 24px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.VPTeamMembersItem.small .affiliation {
|
||||
padding-top: 4px;
|
||||
line-height: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.VPTeamMembersItem.small .desc {
|
||||
padding-top: 12px;
|
||||
line-height: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.VPTeamMembersItem.small .links {
|
||||
margin: 0 -16px -20px;
|
||||
padding: 10px 0 0;
|
||||
}
|
||||
|
||||
.VPTeamMembersItem.medium .profile {
|
||||
padding: 48px 32px;
|
||||
}
|
||||
|
||||
.VPTeamMembersItem.medium .data {
|
||||
padding-top: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.VPTeamMembersItem.medium .avatar {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
}
|
||||
|
||||
.VPTeamMembersItem.medium .name {
|
||||
letter-spacing: 0.15px;
|
||||
line-height: 28px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.VPTeamMembersItem.medium .affiliation {
|
||||
padding-top: 4px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.VPTeamMembersItem.medium .desc {
|
||||
padding-top: 16px;
|
||||
max-width: 288px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.VPTeamMembersItem.medium .links {
|
||||
margin: 0 -16px -12px;
|
||||
padding: 16px 12px 0;
|
||||
}
|
||||
|
||||
.profile {
|
||||
flex-grow: 1;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.data {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
margin: 0 auto;
|
||||
border-radius: 50%;
|
||||
box-shadow: var(--vp-shadow-3);
|
||||
}
|
||||
|
||||
.avatar-img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.name {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.affiliation {
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.org.link {
|
||||
color: var(--vp-c-text-2);
|
||||
transition: color 0.25s;
|
||||
}
|
||||
|
||||
.org.link:hover {
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.desc {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.desc :deep(a) {
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-brand-1);
|
||||
text-decoration-style: dotted;
|
||||
transition: color 0.25s;
|
||||
}
|
||||
|
||||
.links {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.sp-link {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-sponsor);
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
transition: color 0.25s, background-color 0.25s;
|
||||
}
|
||||
|
||||
.sp .sp-link.link:hover,
|
||||
.sp .sp-link.link:focus {
|
||||
outline: none;
|
||||
color: var(--vp-c-white);
|
||||
background-color: var(--vp-c-sponsor);
|
||||
}
|
||||
|
||||
.sp-icon {
|
||||
margin-right: 8px;
|
||||
font-size: 16px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div class="VPTeamPage">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPTeamPage {
|
||||
margin: 96px 0;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.VPTeamPage {
|
||||
margin: 128px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.VPHome :slotted(.VPTeamPageTitle) {
|
||||
border-top: 1px solid var(--vp-c-gutter);
|
||||
padding-top: 88px !important;
|
||||
}
|
||||
|
||||
:slotted(.VPTeamPageSection + .VPTeamPageSection),
|
||||
:slotted(.VPTeamMembers + .VPTeamPageSection) {
|
||||
margin-top: 64px;
|
||||
}
|
||||
|
||||
:slotted(.VPTeamMembers + .VPTeamMembers) {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
:slotted(.VPTeamPageTitle + .VPTeamPageSection) {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
:slotted(.VPTeamPageSection + .VPTeamPageSection),
|
||||
:slotted(.VPTeamMembers + .VPTeamPageSection) {
|
||||
margin-top: 96px;
|
||||
}
|
||||
}
|
||||
|
||||
:slotted(.VPTeamMembers) {
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
:slotted(.VPTeamMembers) {
|
||||
padding: 0 48px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
:slotted(.VPTeamMembers) {
|
||||
padding: 0 64px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<section class="VPTeamPageSection">
|
||||
<div class="title">
|
||||
<div class="title-line" />
|
||||
<h2 v-if="$slots.title" class="title-text">
|
||||
<slot name="title" />
|
||||
</h2>
|
||||
</div>
|
||||
<p v-if="$slots.lead" class="lead">
|
||||
<slot name="lead" />
|
||||
</p>
|
||||
<div v-if="$slots.members" class="members">
|
||||
<slot name="members" />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPTeamPageSection {
|
||||
padding: 0 32px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.VPTeamPageSection {
|
||||
padding: 0 48px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.VPTeamPageSection {
|
||||
padding: 0 64px;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
max-width: 1152px;
|
||||
text-align: center;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.title-line {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.title-text {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding: 0 24px;
|
||||
letter-spacing: 0;
|
||||
line-height: 32px;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
background-color: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
.lead {
|
||||
margin: 0 auto;
|
||||
max-width: 480px;
|
||||
padding-top: 12px;
|
||||
text-align: center;
|
||||
line-height: 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.members {
|
||||
padding-top: 40px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div class="VPTeamPageTitle">
|
||||
<h1 v-if="$slots.title" class="title">
|
||||
<slot name="title" />
|
||||
</h1>
|
||||
<p v-if="$slots.lead" class="lead">
|
||||
<slot name="lead" />
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPTeamPageTitle {
|
||||
padding: 48px 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.VPTeamPageTitle {
|
||||
padding: 64px 48px 48px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.VPTeamPageTitle {
|
||||
padding: 80px 64px 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
letter-spacing: 0;
|
||||
line-height: 44px;
|
||||
font-size: 36px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.title {
|
||||
letter-spacing: -0.5px;
|
||||
line-height: 56px;
|
||||
font-size: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.lead {
|
||||
margin: 0 auto;
|
||||
max-width: 512px;
|
||||
padding-top: 12px;
|
||||
line-height: 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.lead {
|
||||
max-width: 592px;
|
||||
letter-spacing: 0.15px;
|
||||
line-height: 28px;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewBox="0 0 24 24">
|
||||
<path d="M21,11H3c-0.6,0-1-0.4-1-1s0.4-1,1-1h18c0.6,0,1,0.4,1,1S21.6,11,21,11z" />
|
||||
<path d="M21,7H3C2.4,7,2,6.6,2,6s0.4-1,1-1h18c0.6,0,1,0.4,1,1S21.6,7,21,7z" />
|
||||
<path d="M21,15H3c-0.6,0-1-0.4-1-1s0.4-1,1-1h18c0.6,0,1,0.4,1,1S21.6,15,21,15z" />
|
||||
<path d="M21,19H3c-0.6,0-1-0.4-1-1s0.4-1,1-1h18c0.6,0,1,0.4,1,1S21.6,19,21,19z" />
|
||||
</svg>
|
||||
</template>
|
@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewBox="0 0 24 24">
|
||||
<path d="M17,11H3c-0.6,0-1-0.4-1-1s0.4-1,1-1h14c0.6,0,1,0.4,1,1S17.6,11,17,11z" />
|
||||
<path d="M21,7H3C2.4,7,2,6.6,2,6s0.4-1,1-1h18c0.6,0,1,0.4,1,1S21.6,7,21,7z" />
|
||||
<path d="M21,15H3c-0.6,0-1-0.4-1-1s0.4-1,1-1h18c0.6,0,1,0.4,1,1S21.6,15,21,15z" />
|
||||
<path d="M17,19H3c-0.6,0-1-0.4-1-1s0.4-1,1-1h14c0.6,0,1,0.4,1,1S17.6,19,17,19z" />
|
||||
</svg>
|
||||
</template>
|
@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewBox="0 0 24 24">
|
||||
<path d="M21,11H7c-0.6,0-1-0.4-1-1s0.4-1,1-1h14c0.6,0,1,0.4,1,1S21.6,11,21,11z" />
|
||||
<path d="M21,7H3C2.4,7,2,6.6,2,6s0.4-1,1-1h18c0.6,0,1,0.4,1,1S21.6,7,21,7z" />
|
||||
<path d="M21,15H3c-0.6,0-1-0.4-1-1s0.4-1,1-1h18c0.6,0,1,0.4,1,1S21.6,15,21,15z" />
|
||||
<path d="M21,19H7c-0.6,0-1-0.4-1-1s0.4-1,1-1h14c0.6,0,1,0.4,1,1S21.6,19,21,19z" />
|
||||
</svg>
|
||||
</template>
|
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M19,11H7.4l5.3-5.3c0.4-0.4,0.4-1,0-1.4s-1-0.4-1.4,0l-7,7c-0.1,0.1-0.2,0.2-0.2,0.3c-0.1,0.2-0.1,0.5,0,0.8c0.1,0.1,0.1,0.2,0.2,0.3l7,7c0.2,0.2,0.5,0.3,0.7,0.3s0.5-0.1,0.7-0.3c0.4-0.4,0.4-1,0-1.4L7.4,13H19c0.6,0,1-0.4,1-1S19.6,11,19,11z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M19.9,12.4c0.1-0.2,0.1-0.5,0-0.8c-0.1-0.1-0.1-0.2-0.2-0.3l-7-7c-0.4-0.4-1-0.4-1.4,0s-0.4,1,0,1.4l5.3,5.3H5c-0.6,0-1,0.4-1,1s0.4,1,1,1h11.6l-5.3,5.3c-0.4,0.4-0.4,1,0,1.4c0.2,0.2,0.5,0.3,0.7,0.3s0.5-0.1,0.7-0.3l7-7C19.8,12.6,19.9,12.5,19.9,12.4z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewBox="0 0 24 24">
|
||||
<path d="M12,16c-0.3,0-0.5-0.1-0.7-0.3l-6-6c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l5.3,5.3l5.3-5.3c0.4-0.4,1-0.4,1.4,0s0.4,1,0,1.4l-6,6C12.5,15.9,12.3,16,12,16z" />
|
||||
</svg>
|
||||
</template>
|
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewBox="0 0 24 24">
|
||||
<path d="M15,19c-0.3,0-0.5-0.1-0.7-0.3l-6-6c-0.4-0.4-0.4-1,0-1.4l6-6c0.4-0.4,1-0.4,1.4,0s0.4,1,0,1.4L10.4,12l5.3,5.3c0.4,0.4,0.4,1,0,1.4C15.5,18.9,15.3,19,15,19z" />
|
||||
</svg>
|
||||
</template>
|
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewBox="0 0 24 24">
|
||||
<path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z" />
|
||||
</svg>
|
||||
</template>
|
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewBox="0 0 24 24">
|
||||
<path d="M18,16c-0.3,0-0.5-0.1-0.7-0.3L12,10.4l-5.3,5.3c-0.4,0.4-1,0.4-1.4,0s-0.4-1,0-1.4l6-6c0.4-0.4,1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4C18.5,15.9,18.3,16,18,16z" />
|
||||
</svg>
|
||||
</template>
|
@ -0,0 +1,6 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="M18,23H4c-1.7,0-3-1.3-3-3V6c0-1.7,1.3-3,3-3h7c0.6,0,1,0.4,1,1s-0.4,1-1,1H4C3.4,5,3,5.4,3,6v14c0,0.6,0.4,1,1,1h14c0.6,0,1-0.4,1-1v-7c0-0.6,0.4-1,1-1s1,0.4,1,1v7C21,21.7,19.7,23,18,23z" />
|
||||
<path d="M8,17c-0.3,0-0.5-0.1-0.7-0.3C7,16.5,6.9,16.1,7,15.8l1-4c0-0.2,0.1-0.3,0.3-0.5l9.5-9.5c1.2-1.2,3.2-1.2,4.4,0c1.2,1.2,1.2,3.2,0,4.4l-9.5,9.5c-0.1,0.1-0.3,0.2-0.5,0.3l-4,1C8.2,17,8.1,17,8,17zM9.9,12.5l-0.5,2.1l2.1-0.5l9.3-9.3c0.4-0.4,0.4-1.1,0-1.6c-0.4-0.4-1.2-0.4-1.6,0l0,0L9.9,12.5z M18.5,2.5L18.5,2.5L18.5,2.5z" />
|
||||
</svg>
|
||||
</template>
|
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="M12,22.2c-0.3,0-0.5-0.1-0.7-0.3l-8.8-8.8c-2.5-2.5-2.5-6.7,0-9.2c2.5-2.5,6.7-2.5,9.2,0L12,4.3l0.4-0.4c0,0,0,0,0,0C13.6,2.7,15.2,2,16.9,2c0,0,0,0,0,0c1.7,0,3.4,0.7,4.6,1.9l0,0c1.2,1.2,1.9,2.9,1.9,4.6c0,1.7-0.7,3.4-1.9,4.6l-8.8,8.8C12.5,22.1,12.3,22.2,12,22.2zM7,4C5.9,4,4.7,4.4,3.9,5.3c-1.8,1.8-1.8,4.6,0,6.4l8.1,8.1l8.1-8.1c0.9-0.9,1.3-2,1.3-3.2c0-1.2-0.5-2.3-1.3-3.2l0,0C19.3,4.5,18.2,4,17,4c0,0,0,0,0,0c-1.2,0-2.3,0.5-3.2,1.3c0,0,0,0,0,0l-1.1,1.1c-0.4,0.4-1,0.4-1.4,0l-1.1-1.1C9.4,4.4,8.2,4,7,4z" />
|
||||
</svg>
|
||||
</template>
|
@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewBox="0 0 24 24">
|
||||
<path d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path
|
||||
d=" M12.87 15.07l-2.54-2.51.03-.03c1.74-1.94 2.98-4.17 3.71-6.53H17V4h-7V2H8v2H1v1.99h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12zm-2.62 7l1.62-4.33L19.12 17h-3.24z "
|
||||
class="css-c4d79v"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="M22,13H2a1,1,0,0,1,0-2H22a1,1,0,0,1,0,2Z" />
|
||||
</svg>
|
||||
</template>
|
@ -0,0 +1,6 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 24 24">
|
||||
<path d="M19,2H5C3.3,2,2,3.3,2,5v14c0,1.7,1.3,3,3,3h14c1.7,0,3-1.3,3-3V5C22,3.3,20.7,2,19,2zM20,19c0,0.6-0.4,1-1,1H5c-0.6,0-1-0.4-1-1V5c0-0.6,0.4-1,1-1h14c0.6,0,1,0.4,1,1V19z" />
|
||||
<path d="M16,11H8c-0.6,0-1,0.4-1,1s0.4,1,1,1h8c0.6,0,1-0.4,1-1S16.6,11,16,11z" />
|
||||
</svg>
|
||||
</template>
|
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewBox="0 0 24 24">
|
||||
<path d="M12.1,22c-0.3,0-0.6,0-0.9,0c-5.5-0.5-9.5-5.4-9-10.9c0.4-4.8,4.2-8.6,9-9c0.4,0,0.8,0.2,1,0.5c0.2,0.3,0.2,0.8-0.1,1.1c-2,2.7-1.4,6.4,1.3,8.4c2.1,1.6,5,1.6,7.1,0c0.3-0.2,0.7-0.3,1.1-0.1c0.3,0.2,0.5,0.6,0.5,1c-0.2,2.7-1.5,5.1-3.6,6.8C16.6,21.2,14.4,22,12.1,22zM9.3,4.4c-2.9,1-5,3.6-5.2,6.8c-0.4,4.4,2.8,8.3,7.2,8.7c2.1,0.2,4.2-0.4,5.8-1.8c1.1-0.9,1.9-2.1,2.4-3.4c-2.5,0.9-5.3,0.5-7.5-1.1C9.2,11.4,8.1,7.7,9.3,4.4z" />
|
||||
</svg>
|
||||
</template>
|
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="2" />
|
||||
<circle cx="19" cy="12" r="2" />
|
||||
<circle cx="5" cy="12" r="2" />
|
||||
</svg>
|
||||
</template>
|
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewBox="0 0 24 24">
|
||||
<path d="M18.9,10.9h-6v-6c0-0.6-0.4-1-1-1s-1,0.4-1,1v6h-6c-0.6,0-1,0.4-1,1s0.4,1,1,1h6v6c0,0.6,0.4,1,1,1s1-0.4,1-1v-6h6c0.6,0,1-0.4,1-1S19.5,10.9,18.9,10.9z" />
|
||||
</svg>
|
||||
</template>
|
@ -0,0 +1,6 @@
|
||||
<template>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="M19,2H5C3.3,2,2,3.3,2,5v14c0,1.7,1.3,3,3,3h14c1.7,0,3-1.3,3-3V5C22,3.3,20.7,2,19,2z M20,19c0,0.6-0.4,1-1,1H5c-0.6,0-1-0.4-1-1V5c0-0.6,0.4-1,1-1h14c0.6,0,1,0.4,1,1V19z" />
|
||||
<path d="M16,11h-3V8c0-0.6-0.4-1-1-1s-1,0.4-1,1v3H8c-0.6,0-1,0.4-1,1s0.4,1,1,1h3v3c0,0.6,0.4,1,1,1s1-0.4,1-1v-3h3c0.6,0,1-0.4,1-1S16.6,11,16,11z" />
|
||||
</svg>
|
||||
</template>
|
@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewBox="0 0 24 24">
|
||||
<path d="M12,18c-3.3,0-6-2.7-6-6s2.7-6,6-6s6,2.7,6,6S15.3,18,12,18zM12,8c-2.2,0-4,1.8-4,4c0,2.2,1.8,4,4,4c2.2,0,4-1.8,4-4C16,9.8,14.2,8,12,8z" />
|
||||
<path d="M12,4c-0.6,0-1-0.4-1-1V1c0-0.6,0.4-1,1-1s1,0.4,1,1v2C13,3.6,12.6,4,12,4z" />
|
||||
<path d="M12,24c-0.6,0-1-0.4-1-1v-2c0-0.6,0.4-1,1-1s1,0.4,1,1v2C13,23.6,12.6,24,12,24z" />
|
||||
<path d="M5.6,6.6c-0.3,0-0.5-0.1-0.7-0.3L3.5,4.9c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l1.4,1.4c0.4,0.4,0.4,1,0,1.4C6.2,6.5,5.9,6.6,5.6,6.6z" />
|
||||
<path d="M19.8,20.8c-0.3,0-0.5-0.1-0.7-0.3l-1.4-1.4c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l1.4,1.4c0.4,0.4,0.4,1,0,1.4C20.3,20.7,20,20.8,19.8,20.8z" />
|
||||
<path d="M3,13H1c-0.6,0-1-0.4-1-1s0.4-1,1-1h2c0.6,0,1,0.4,1,1S3.6,13,3,13z" />
|
||||
<path d="M23,13h-2c-0.6,0-1-0.4-1-1s0.4-1,1-1h2c0.6,0,1,0.4,1,1S23.6,13,23,13z" />
|
||||
<path d="M4.2,20.8c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l1.4-1.4c0.4-0.4,1-0.4,1.4,0s0.4,1,0,1.4l-1.4,1.4C4.7,20.7,4.5,20.8,4.2,20.8z" />
|
||||
<path d="M18.4,6.6c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l1.4-1.4c0.4-0.4,1-0.4,1.4,0s0.4,1,0,1.4l-1.4,1.4C18.9,6.5,18.6,6.6,18.4,6.6z" />
|
||||
</svg>
|
||||
</template>
|
@ -0,0 +1,21 @@
|
||||
import { useMediaQuery } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
import { useLayout } from './layout'
|
||||
|
||||
export function useAside() {
|
||||
const { hasSidebar } = useLayout()
|
||||
const is960 = useMediaQuery('(min-width: 960px)')
|
||||
const is1280 = useMediaQuery('(min-width: 1280px)')
|
||||
|
||||
const isAsideEnabled = computed(() => {
|
||||
if (!is1280.value && !is960.value) {
|
||||
return false
|
||||
}
|
||||
|
||||
return hasSidebar.value ? is1280.value : is960.value
|
||||
})
|
||||
|
||||
return {
|
||||
isAsideEnabled
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
import { useData as useData$ } from 'vitepress'
|
||||
import type { DefaultTheme } from 'vitepress/theme'
|
||||
|
||||
export const useData: typeof useData$<DefaultTheme.Config> = useData$
|
@ -0,0 +1,18 @@
|
||||
import { computed } from 'vue'
|
||||
import { useData } from './data'
|
||||
|
||||
export function useEditLink() {
|
||||
const { theme, page } = useData()
|
||||
|
||||
return computed(() => {
|
||||
const { text = 'Edit this page', pattern = '' } = theme.value.editLink || {}
|
||||
let url: string
|
||||
if (typeof pattern === 'function') {
|
||||
url = pattern(page.value)
|
||||
} else {
|
||||
url = pattern.replace(/:path/g, page.value.filePath)
|
||||
}
|
||||
|
||||
return { url, text }
|
||||
})
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
import { onUnmounted, readonly, type Ref, ref, watch } from 'vue'
|
||||
import { inBrowser } from '../../shared'
|
||||
|
||||
interface UseFlyoutOptions {
|
||||
el: Ref<HTMLElement | undefined>
|
||||
onFocus?(): void
|
||||
onBlur?(): void
|
||||
}
|
||||
|
||||
export const focusedElement = ref<HTMLElement>()
|
||||
|
||||
let active = false
|
||||
let listeners = 0
|
||||
|
||||
export function useFlyout(options: UseFlyoutOptions) {
|
||||
const focus = ref(false)
|
||||
|
||||
if (inBrowser) {
|
||||
!active && activateFocusTracking()
|
||||
|
||||
listeners++
|
||||
|
||||
const unwatch = watch(focusedElement, (el) => {
|
||||
if (el === options.el.value || options.el.value?.contains(el!)) {
|
||||
focus.value = true
|
||||
options.onFocus?.()
|
||||
} else {
|
||||
focus.value = false
|
||||
options.onBlur?.()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
unwatch()
|
||||
|
||||
listeners--
|
||||
|
||||
if (!listeners) {
|
||||
deactivateFocusTracking()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return readonly(focus)
|
||||
}
|
||||
|
||||
function activateFocusTracking() {
|
||||
document.addEventListener('focusin', handleFocusIn)
|
||||
active = true
|
||||
focusedElement.value = document.activeElement as HTMLElement
|
||||
}
|
||||
|
||||
function deactivateFocusTracking() {
|
||||
document.removeEventListener('focusin', handleFocusIn)
|
||||
}
|
||||
|
||||
function handleFocusIn() {
|
||||
focusedElement.value = document.activeElement as HTMLElement
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
import { computed } from 'vue'
|
||||
import { ensureStartingSlash } from '../support/utils'
|
||||
import { useData } from './data'
|
||||
|
||||
export function useLangs({ correspondingLink = false } = {}) {
|
||||
const { site, localeIndex, page, theme, hash } = useData()
|
||||
const currentLang = computed(() => ({
|
||||
label: site.value.locales[localeIndex.value]?.label,
|
||||
link:
|
||||
site.value.locales[localeIndex.value]?.link ||
|
||||
(localeIndex.value === 'root' ? '/' : `/${localeIndex.value}/`)
|
||||
}))
|
||||
|
||||
const localeLinks = computed(() =>
|
||||
Object.entries(site.value.locales).flatMap(([key, value]) =>
|
||||
currentLang.value.label === value.label
|
||||
? []
|
||||
: {
|
||||
text: value.label,
|
||||
link:
|
||||
normalizeLink(
|
||||
value.link || (key === 'root' ? '/' : `/${key}/`),
|
||||
theme.value.i18nRouting !== false && correspondingLink,
|
||||
page.value.relativePath.slice(
|
||||
currentLang.value.link.length - 1
|
||||
),
|
||||
!site.value.cleanUrls
|
||||
) + hash.value
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
return { localeLinks, currentLang }
|
||||
}
|
||||
|
||||
function normalizeLink(
|
||||
link: string,
|
||||
addPath: boolean,
|
||||
path: string,
|
||||
addExt: boolean
|
||||
) {
|
||||
return addPath
|
||||
? link.replace(/\/$/, '') +
|
||||
ensureStartingSlash(
|
||||
path
|
||||
.replace(/(^|\/)index\.md$/, '$1')
|
||||
.replace(/\.md$/, addExt ? '.html' : '')
|
||||
)
|
||||
: link
|
||||
}
|
@ -0,0 +1,118 @@
|
||||
import { inBrowser, onContentUpdated, useRoute } from 'vitepress'
|
||||
import type { DefaultTheme, useLayout as expected } from 'vitepress/theme'
|
||||
import {
|
||||
computed,
|
||||
shallowReadonly,
|
||||
shallowRef,
|
||||
watch,
|
||||
type ComputedRef,
|
||||
type InjectionKey
|
||||
} from 'vue'
|
||||
import { getSidebar, getSidebarGroups } from '../support/sidebar'
|
||||
import { useData } from './data'
|
||||
import { getHeaders } from './outline'
|
||||
import { useCloseSidebarOnEscape } from './sidebar'
|
||||
|
||||
const headers = shallowRef<DefaultTheme.OutlineItem[]>([])
|
||||
const sidebar = shallowRef<DefaultTheme.SidebarItem[]>([])
|
||||
|
||||
const is960 = shallowRef(false)
|
||||
|
||||
export function useLayout(): ReturnType<typeof expected> {
|
||||
const { frontmatter, theme } = useData()
|
||||
|
||||
const isHome = computed(() => {
|
||||
return !!(frontmatter.value.isHome ?? frontmatter.value.layout === 'home')
|
||||
})
|
||||
|
||||
const hasSidebar = computed(() => {
|
||||
return (
|
||||
frontmatter.value.sidebar !== false &&
|
||||
sidebar.value.length > 0 &&
|
||||
!isHome.value
|
||||
)
|
||||
})
|
||||
|
||||
const isSidebarEnabled = computed(() => hasSidebar.value && is960.value)
|
||||
|
||||
const sidebarGroups = computed(() => {
|
||||
return hasSidebar.value ? getSidebarGroups(sidebar.value) : []
|
||||
})
|
||||
|
||||
const hasAside = computed(() => {
|
||||
if (isHome.value) return false
|
||||
if (frontmatter.value.aside != null) return !!frontmatter.value.aside
|
||||
return theme.value.aside !== false
|
||||
})
|
||||
|
||||
const leftAside = computed(() => {
|
||||
if (!hasAside.value) return false
|
||||
return frontmatter.value.aside == null
|
||||
? theme.value.aside === 'left'
|
||||
: frontmatter.value.aside === 'left'
|
||||
})
|
||||
|
||||
const hasLocalNav = computed(() => {
|
||||
return headers.value.length > 0
|
||||
})
|
||||
|
||||
return {
|
||||
isHome,
|
||||
sidebar: shallowReadonly(sidebar),
|
||||
sidebarGroups,
|
||||
hasSidebar,
|
||||
isSidebarEnabled,
|
||||
hasAside,
|
||||
leftAside,
|
||||
headers: shallowReadonly(headers),
|
||||
hasLocalNav
|
||||
}
|
||||
}
|
||||
|
||||
interface RegisterWatchersOptions {
|
||||
closeSidebar: () => void
|
||||
}
|
||||
|
||||
export function registerWatchers({ closeSidebar }: RegisterWatchersOptions) {
|
||||
const { frontmatter, page, theme } = useData()
|
||||
|
||||
watch(
|
||||
() => [page.value.relativePath, theme.value.sidebar] as const,
|
||||
([relativePath, sidebarConfig]) => {
|
||||
const newSidebar = sidebarConfig
|
||||
? getSidebar(sidebarConfig, relativePath)
|
||||
: []
|
||||
if (JSON.stringify(newSidebar) !== JSON.stringify(sidebar.value)) {
|
||||
sidebar.value = newSidebar
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true, flush: 'sync' }
|
||||
)
|
||||
|
||||
onContentUpdated(() => {
|
||||
headers.value = getHeaders(frontmatter.value.outline ?? theme.value.outline)
|
||||
})
|
||||
|
||||
if (inBrowser) {
|
||||
is960.value = window.innerWidth >= 960
|
||||
window.addEventListener(
|
||||
'resize',
|
||||
() => {
|
||||
is960.value = window.innerWidth >= 960
|
||||
},
|
||||
{ passive: true }
|
||||
)
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
watch(() => route.path, closeSidebar)
|
||||
|
||||
useCloseSidebarOnEscape(closeSidebar)
|
||||
}
|
||||
|
||||
export interface LayoutInfo {
|
||||
heroImageSlotExists: ComputedRef<boolean>
|
||||
}
|
||||
|
||||
export const layoutInfoInjectionKey: InjectionKey<LayoutInfo> =
|
||||
Symbol('layout-info')
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue