chore: add vitepress farris theme

This commit is contained in:
Sagi 2022-10-05 22:20:40 +08:00
parent c9420ea44e
commit 572e0dcd97
134 changed files with 7974 additions and 2 deletions

View File

@ -0,0 +1,75 @@
<script setup lang="ts">
import { provide, watch } from 'vue'
import { useData, useRoute } from 'vitepress'
import { useSidebar, useCloseSidebarOnEscape } from './composables/sidebar.js'
import VPSkipLink from './components/VPSkipLink.vue'
import VPBackdrop from './components/VPBackdrop.vue'
import VPNav from './components/VPNav.vue'
import VPLocalNav from './components/VPLocalNav.vue'
import VPSidebar from './components/VPSidebar.vue'
import VPContent from './components/VPContent.vue'
import VPFooter from './components/VPFooter.vue'
const {
isOpen: isSidebarOpen,
open: openSidebar,
close: closeSidebar
} = useSidebar()
const route = useRoute()
watch(() => route.path, closeSidebar)
useCloseSidebarOnEscape(isSidebarOpen, closeSidebar)
provide('close-sidebar', closeSidebar)
const { frontmatter } = useData()
</script>
<template>
<div v-if="frontmatter.layout !== false" class="Layout">
<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" />
<VPContent>
<template #home-hero-before><slot name="home-hero-before" /></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 #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 scoped>
.Layout {
display: flex;
flex-direction: column;
min-height: 100vh;
}
</style>

View File

@ -0,0 +1,84 @@
<script setup lang="ts">
import { useData } from 'vitepress'
const { site } = useData()
</script>
<template>
<div class="NotFound">
<p class="code">404</p>
<h1 class="title">PAGE NOT FOUND</h1>
<div class="divider" />
<blockquote class="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="site.base" aria-label="go to home">
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);
border-radius: 16px;
padding: 3px 16px;
font-size: 14px;
font-weight: 500;
color: var(--vp-c-brand);
transition: border-color 0.25s, color .25s;
}
.link:hover {
border-color: var(--vp-c-brand-dark);
color: var(--vp-c-brand-dark);
}
</style>

View File

@ -0,0 +1,94 @@
<script setup lang="ts">
import type { DefaultTheme } from 'vitepress/theme'
import docsearch from '@docsearch/js'
import { onMounted } from 'vue'
import { useRouter, useRoute, useData } from 'vitepress'
const router = useRouter()
const route = useRoute()
const { theme, site } = useData()
onMounted(() => {
initialize(theme.value.algolia)
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)
}
const docsearch$ = docsearch.default ?? docsearch
type DocSearchProps = Parameters<typeof docsearch$>[0]
function initialize(userOptions: DefaultTheme.AlgoliaSearchOptions) {
// note: multi-lang search support is removed since the theme
// doesn't support multiple locales as of now.
const options = Object.assign<{}, {}, DocSearchProps>({}, userOptions, {
container: '#docsearch',
navigator: {
navigate({ itemUrl }) {
const { pathname: hitPathname } = new URL(
window.location.origin + itemUrl
)
// router doesn't handle same-page navigation so we use the native
// browser location API for anchor navigation
if (route.path === hitPathname) {
window.location.assign(window.location.origin + itemUrl)
} else {
router.go(itemUrl)
}
}
},
transformItems(items) {
return items.map((item) => {
return Object.assign({}, item, {
url: getRelativePath(item.url)
})
})
},
// @ts-expect-error vue-tsc thinks this should return Vue JSX but it returns the required React one
hitComponent({ hit, children }) {
return {
__v: null,
type: 'a',
ref: undefined,
constructor: undefined,
key: undefined,
props: { href: hit.url, children }
}
}
})
docsearch$(options)
}
function getRelativePath(absoluteUrl: string) {
const { pathname, hash } = new URL(absoluteUrl)
return (
pathname.replace(
/\.html$/,
site.value.cleanUrls === 'disabled' ? '.html' : ''
) + hash
)
}
</script>
<template>
<div id="docsearch" />
</template>

View File

@ -0,0 +1,39 @@
<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;
right: 0;
bottom: 0;
left: 0;
z-index: var(--vp-z-index-backdrop);
background: rgba(0, 0, 0, .6);
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>

View File

@ -0,0 +1,124 @@
<script setup lang="ts">
import { computed } from 'vue'
import { normalizeLink } from '../support/utils.ts'
import { EXTERNAL_URL_RE } from '../shared/shared.ts'
const props = defineProps<{
tag?: string
size?: 'medium' | 'big'
theme?: 'brand' | 'alt' | 'sponsor'
text: string
href?: string
}>()
const classes = computed(() => [
props.size ?? 'medium',
props.theme ?? 'brand'
])
const isExternal = computed(() => props.href && EXTERNAL_URL_RE.test(props.href))
const component = computed(() => {
if (props.tag) {
return props.tag
}
return props.href ? 'a' : 'button'
})
</script>
<template>
<component
:is="component"
class="VPButton"
:class="classes"
:href="href ? normalizeLink(href) : undefined"
:target="isExternal ? '_blank' : undefined"
:rel="isExternal ? 'noreferrer' : undefined"
>
{{ text }}
</component>
</template>
<style scoped>
.VPButton {
display: inline-block;
border: 1px solid transparent;
text-align: center;
font-weight: 500;
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>

View File

@ -0,0 +1,90 @@
<script setup lang="ts">
import { ref, watch, onMounted } from 'vue'
import { useData } from 'vitepress'
import { useAside } from '../composables/aside.ts'
const { theme } = useData()
const carbonOptions = theme.value.carbonAds
const { isAsideEnabled } = useAside()
const container = ref()
let hasInitalized = false
function init() {
if (!hasInitalized) {
hasInitalized = 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)
}
}
// 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>
.VPCarbonAds {
display: flex;
justify-content: center;
align-items: center;
padding: 24px;
border-radius: 12px;
min-height: 240px;
text-align: center;
line-height: 18px;
font-size: 12px;
font-weight: 500;
background-color: var(--vp-c-bg-soft);
transition: color 0.5s, background-color 0.5s;
}
.VPCarbonAds img {
margin: 0 auto;
border-radius: 6px;
}
.VPCarbonAds .carbon-text {
display: block;
margin: 0 auto;
padding-top: 12px;
color: var(--vp-c-text-1);
transition: color 0.25s;
}
.VPCarbonAds .carbon-text:hover {
color: var(--vp-c-brand);
}
.VPCarbonAds .carbon-poweredby {
display: block;
padding-top: 6px;
font-size: 11px;
font-weight: 500;
color: var(--vp-c-text-2);
text-transform: uppercase;
transition: color 0.25s;
}
.VPCarbonAds .carbon-poweredby:hover {
color: var(--vp-c-text-1);
}
</style>

View File

@ -0,0 +1,81 @@
<script setup lang="ts">
import { useRoute, useData } from 'vitepress'
import { useSidebar } from '../composables/sidebar.ts'
import VPPage from './VPPage.vue'
import VPHome from './VPHome.vue'
import VPDoc from './VPDoc.vue'
import { inject } from 'vue'
const route = useRoute()
const { frontmatter } = useData()
const { hasSidebar } = useSidebar()
const NotFound = inject('NotFound')
</script>
<template>
<div
class="VPContent"
id="VPContent"
:class="{
'has-sidebar': hasSidebar,
'is-home': frontmatter.layout === 'home'
}"
>
<NotFound v-if="route.component === NotFound" />
<VPPage v-else-if="frontmatter.layout === 'page'" />
<VPHome v-else-if="frontmatter.layout === 'home'">
<template #home-hero-before><slot name="home-hero-before" /></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>
<VPDoc v-else>
<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: 0 auto;
width: 100%;
}
.VPContent.is-home {
width: 100%;
max-width: 100%;
}
@media (min-width: 960px) {
.VPContent {
padding-top: var(--vp-nav-height);
}
.VPContent.has-sidebar {
margin: 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>

View File

@ -0,0 +1,178 @@
<script setup lang="ts">
import { useRoute } from 'vitepress'
import { computed, provide, ref } from 'vue'
import { useSidebar } from '../composables/sidebar.ts'
import VPDocAside from './VPDocAside.vue'
import VPDocFooter from './VPDocFooter.vue'
const route = useRoute()
const { hasSidebar, hasAside } = useSidebar()
const pageName = computed(() =>
route.path.replace(/[./]+/g, '_').replace(/_html$/, '')
)
const onContentUpdated = ref()
provide('onContentUpdated', onContentUpdated)
</script>
<template>
<div
class="VPDoc"
:class="{ 'has-sidebar': hasSidebar, 'has-aside': hasAside }"
>
<div class="container">
<div v-if="hasAside" class="aside">
<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" :onContentUpdated="onContentUpdated" />
</main>
<slot name="doc-footer-before" />
<VPDocFooter />
<slot name="doc-after" />
</div>
</div>
</div>
</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: 32px 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;
}
.aside-container {
position: sticky;
top: 0;
margin-top: calc(var(--vp-nav-height-desktop) * -1 - 32px);
padding-top: calc(var(--vp-nav-height-desktop) + 32px);
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%);
}
.aside-content {
display: flex;
flex-direction: column;
min-height: calc(100vh - (var(--vp-nav-height-desktop) + 32px));
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>

View File

@ -0,0 +1,46 @@
<script setup lang="ts">
import { useData } from 'vitepress'
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" />
<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>

View File

@ -0,0 +1,13 @@
<script setup lang="ts">
import { defineAsyncComponent } from 'vue'
const VPCarbonAds = __CARBON__
? defineAsyncComponent(() => import('./VPCarbonAds.vue'))
: () => null
</script>
<template>
<div class="VPDocAsideCarbonAds">
<VPCarbonAds />
</div>
</template>

View File

@ -0,0 +1,94 @@
<script setup lang="ts">
import { useData } from 'vitepress'
import type { DefaultTheme } from 'vitepress/theme'
import { computed, inject, ref, type Ref } from 'vue'
import {
getHeaders,
useActiveAnchor,
type MenuItem
} from '../composables/outline.ts'
import VPDocAsideOutlineItem from './VPDocAsideOutlineItem.vue'
const { frontmatter, theme } = useData()
const pageOutline = computed<DefaultTheme.Config['outline']>(
() => frontmatter.value.outline ?? theme.value.outline
)
const onContentUpdated = inject('onContentUpdated') as Ref<() => void>
onContentUpdated.value = () => {
headers.value = getHeaders(pageOutline.value)
}
const headers = ref<MenuItem[]>([])
const hasOutline = computed(() => headers.value.length > 0)
const container = ref()
const marker = ref()
useActiveAnchor(container, marker)
function handleClick({ target: el }: Event) {
const id = '#' + (el as HTMLAnchorElement).href!.split('#')[1]
const heading = document.querySelector<HTMLAnchorElement>(
decodeURIComponent(id)
)
heading?.focus()
}
</script>
<template>
<div class="VPDocAsideOutline" :class="{ 'has-outline': hasOutline }" ref="container">
<div class="content">
<div class="outline-marker" ref="marker" />
<div class="outline-title">
{{ theme.outlineTitle || 'On this page' }}
</div>
<nav aria-labelledby="doc-outline-aria-label">
<span class="visually-hidden" id="doc-outline-aria-label">
Table of Contents for current page
</span>
<VPDocAsideOutlineItem :headers="headers" :root="true" :onClick="handleClick" />
</nav>
</div>
</div>
</template>
<style scoped>
.VPDocAsideOutline {
display: none;
}
.VPDocAsideOutline.has-outline {
display: block;
}
.content {
position: relative;
border-left: 1px solid var(--vp-c-divider-light);
padding-left: 16px;
font-size: 13px;
font-weight: 500;
}
.outline-marker {
position: absolute;
top: 32px;
left: -1px;
z-index: 0;
opacity: 0;
width: 1px;
height: 18px;
background-color: var(--vp-c-brand);
transition: top 0.25s cubic-bezier(0, 1, 0.5, 1), background-color 0.5s, opacity 0.25s;
}
.outline-title {
letter-spacing: 0.4px;
line-height: 28px;
font-size: 13px;
font-weight: 600;
}
</style>

View File

@ -0,0 +1,51 @@
<script setup lang="ts">
import type { MenuItem } from '../composables/outline.ts'
defineProps<{
headers: MenuItem[]
onClick: (e: MouseEvent) => void
root?: boolean
}>()
</script>
<template>
<ul :class="root ? 'root' : 'nested'">
<li v-for="{ children, link, title } in headers">
<a class="outline-link" :href="link" @click="onClick">{{ title }}</a>
<template v-if="children?.length">
<VPDocAsideOutlineItem :headers="children" :onClick="onClick" />
</template>
</li>
</ul>
</template>
<style scoped>
.root {
position: relative;
z-index: 1;
}
.nested {
padding-left: 13px;
}
.outline-link {
display: block;
line-height: 28px;
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>

View File

@ -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="tier" :size="size" :data="data" />
</div>
</template>

View File

@ -0,0 +1,167 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useData } from 'vitepress'
import { normalizeLink } from '../support/utils.js'
import { useEditLink } from '../composables/edit-link.js'
import { usePrevNext } from '../composables/prev-next.js'
import VPIconEdit from './icons/VPIconEdit.vue'
import VPLink from './VPLink.vue'
import VPDocFooterLastUpdated from './VPDocFooterLastUpdated.vue'
const { theme, page, frontmatter } = useData()
const editLink = useEditLink()
const control = usePrevNext()
const hasEditLink = computed(() => {
return theme.value.editLink && frontmatter.value.editLink !== false
})
const hasLastUpdated = computed(() => {
return page.value.lastUpdated && frontmatter.value.lastUpdated !== false
})
const showFooter = computed(() => {
return hasEditLink.value || hasLastUpdated.value || control.value.prev || control.value.next
})
</script>
<template>
<footer v-if="showFooter" class="VPDocFooter">
<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">
<VPIconEdit class="edit-link-icon" />
{{ editLink.text }}
</VPLink>
</div>
<div v-if="hasLastUpdated" class="last-updated">
<VPDocFooterLastUpdated />
</div>
</div>
<div v-if="control.prev || control.next" class="prev-next">
<div class="pager">
<a v-if="control.prev" class="pager-link prev" :href="normalizeLink(control.prev.link)">
<span class="desc">{{ theme.docFooter?.prev ?? 'Previous page' }}</span>
<span class="title">{{ control.prev.text }} </span>
</a>
</div>
<div class="pager" :class="{ 'has-prev': control.prev }">
<a v-if="control.next" class="pager-link next" :href="normalizeLink(control.next.link)">
<span class="desc">{{ theme.docFooter?.next ?? 'Next page' }}</span>
<span class="title">{{ control.next.text }}</span>
</a>
</div>
</div>
</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);
transition: color 0.25s;
}
.edit-link-button:hover {
color: var(--vp-c-brand-dark);
}
.edit-link-icon {
margin-right: 8px;
width: 14px;
height: 14px;
fill: currentColor;
}
.prev-next {
border-top: 1px solid var(--vp-c-divider-light);
padding-top: 24px;
}
@media (min-width: 640px) {
.prev-next {
display: flex;
}
}
.pager.has-prev {
padding-top: 8px;
}
@media (min-width: 640px) {
.pager {
display: flex;
flex-direction: column;
flex-shrink: 0;
width: 50%;
}
.pager.has-prev {
padding-top: 0;
padding-left: 16px;
}
}
.pager-link {
display: block;
border: 1px solid var(--vp-c-divider-light);
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);
}
.pager-link:hover .title {
color: var(--vp-c-brand-dark);
}
.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);
transition: color 0.25s;
}
</style>

View File

@ -0,0 +1,43 @@
<script setup lang="ts">
import { ref, computed, watchEffect, onMounted } from 'vue'
import { useData } from 'vitepress'
const { theme, page } = useData()
const date = computed(() => new Date(page.value.lastUpdated!))
const isoDatetime = computed(() => date.value.toISOString())
const datetime = ref('')
// set time on mounted hook because the locale string might be different
// based on end user and will lead to potential hydration mismatch if
// calculated at build time
onMounted(() => {
watchEffect(() => {
datetime.value = date.value.toLocaleString(window.navigator.language)
})
})
</script>
<template>
<p class="VPLastUpdated">
{{ theme.lastUpdatedText ?? 'Last updated' }}:
<time :datatime="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>

View File

@ -0,0 +1,55 @@
<script setup lang="ts">
defineProps<{
icon?: string
title: string
details: string
}>()
</script>
<template>
<article class="VPFeature">
<div v-if="icon" class="icon">{{ icon }}</div>
<h2 class="title">{{ title }}</h2>
<p class="details">{{ details }}</p>
</article>
</template>
<style scoped>
.VPFeature {
border: 1px solid var(--vp-c-bg-soft);
border-radius: 12px;
padding: 24px;
height: 100%;
background-color: var(--vp-c-bg-soft);
}
.icon {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 20px;
border-radius: 6px;
background-color: var(--vp-c-gray-light-4);
width: 48px;
height: 48px;
font-size: 24px;
}
.dark .icon {
background-color: var(--vp-c-bg);
}
.title {
line-height: 24px;
font-size: 16px;
font-weight: 600;
}
.details {
padding-top: 8px;
line-height: 24px;
font-size: 14px;
font-weight: 500;
color: var(--vp-c-text-2);
}
</style>

View File

@ -0,0 +1,107 @@
<script setup lang="ts">
import { computed } from 'vue'
import VPFeature from './VPFeature.vue'
export interface Feature {
icon?: string
title: string
details: 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 % 2 === 0) {
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"
/>
</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>

View File

@ -0,0 +1,156 @@
<script lang="ts" setup>
import { ref } from 'vue'
import { useFlyout } from '../composables/flyout.js'
import VPIconChevronDown from './icons/VPIconChevronDown.vue'
import VPIconMoreHorizontal from './icons/VPIconMoreHorizontal.vue'
import VPMenu from './VPMenu.vue'
defineProps<{
icon?: any
button?: string
label?: string
items?: any[]
}>()
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">
<component v-if="icon" :is="icon" class="option-icon" />
{{ button }}
<VPIconChevronDown class="text-icon" />
</span>
<VPIconMoreHorizontal v-else class="icon" />
</button>
<div class="menu">
<VPMenu :items="items">
<slot />
</VPMenu>
</div>
</div>
</template>
<style scoped>
.VPFlyout {
position: relative;
}
.VPFlyout:hover {
color: var(--vp-c-brand);
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);
}
.VPFlyout.active:hover .text {
color: var(--vp-c-brand-dark);
}
.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-mobile);
color: var(--vp-c-text-1);
transition: color 0.5s;
}
@media (min-width: 960px) {
.button {
height: var(--vp-nav-height-desktop);
}
}
.text {
display: flex;
align-items: center;
line-height: var(--vp-nav-height-mobile);
font-size: 14px;
font-weight: 500;
color: var(--vp-c-text-1);
transition: color 0.25s;
}
@media (min-width: 960px) {
.text {
line-height: var(--vp-nav-height-desktop);
}
}
.option-icon {
margin-right: 0px;
width: 16px;
height: 16px;
fill: currentColor;
}
.text-icon {
margin-left: 4px;
width: 14px;
height: 14px;
fill: currentColor;
}
.icon {
width: 20px;
height: 20px;
fill: currentColor;
transition: fill 0.25s;
}
.menu {
position: absolute;
top: calc(var(--vp-nav-height-mobile) / 2 + 20px);
right: 0;
opacity: 0;
visibility: hidden;
transition: opacity 0.25s, visibility 0.25s, transform 0.25s;
}
@media (min-width: 960px) {
.menu {
top: calc(var(--vp-nav-height-desktop) / 2 + 20px);
}
}
</style>

View File

@ -0,0 +1,53 @@
<script setup lang="ts">
import { useData } from 'vitepress'
import { useSidebar } from '../composables/sidebar.js'
const { theme } = useData()
const { hasSidebar } = useSidebar()
</script>
<template>
<footer v-if="theme.footer" 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-divider-light);
padding: 32px 24px;
background-color: var(--vp-c-bg);
}
.VPFooter.has-sidebar {
display: none;
}
@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);
}
.message { order: 2; }
.copyright { order: 1; }
</style>

View File

@ -0,0 +1,314 @@
<script setup lang="ts">
import type { DefaultTheme } from 'vitepress/theme'
import VPButton from './VPButton.vue'
import VPImage from './VPImage.vue'
export interface HeroAction {
theme?: 'brand' | 'alt'
text: string
link: string
}
defineProps<{
name?: string
text: string
tagline?: string
image?: DefaultTheme.ThemeableImage
actions?: HeroAction[]
}>()
</script>
<template>
<div class="VPHero" :class="{ 'has-image': image }">
<div class="container">
<div class="main">
<h1 v-if="name" class="name">
<span class="clip">{{ name }}</span>
</h1>
<p v-if="text" class="text">{{ text }}</p>
<p v-if="tagline" class="tagline">{{ tagline }}</p>
<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"
/>
</div>
</div>
</div>
<div v-if="image" class="image">
<div class="image-container">
<div class="image-bg" />
<VPImage class="image-src" :image="image" />
</div>
</div>
</div>
</div>
</template>
<style scoped>
.VPHero {
margin-top: calc(var(--vp-nav-height) * -1);
padding: calc(var(--vp-nav-height) + 48px) 24px 48px;
}
@media (min-width: 640px) {
.VPHero {
padding: calc(var(--vp-nav-height) + 80px) 48px 64px;
}
}
@media (min-width: 960px) {
.VPHero {
padding: calc(var(--vp-nav-height) + 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;
}
}
.name,
.text {
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%;
transform: translate(-32px, -32px);
}
}
.image-bg {
position: absolute;
top: 50%;
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);
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%;
left: 50%;
max-width: 192px;
transform: translate(-50%, -50%);
}
@media (min-width: 640px) {
:deep(.image-src) {
max-width: 256px;
}
}
@media (min-width: 960px) {
:deep(.image-src) {
max-width: 320px;
}
}
</style>

View File

@ -0,0 +1,35 @@
<script setup lang="ts">
import VPHomeHero from './VPHomeHero.vue'
import VPHomeFeatures from './VPHomeFeatures.vue'
</script>
<template>
<div class="VPHome">
<slot name="home-hero-before" />
<VPHomeHero />
<slot name="home-hero-after" />
<slot name="home-features-before" />
<VPHomeFeatures />
<slot name="home-features-after" />
<Content />
</div>
</template>
<style scoped>
.VPHome {
padding-bottom: 96px;
}
.VPHome :deep(.VPHomeSponsors) {
margin-top: 112px;
margin-bottom: -128px;
}
@media (min-width: 768px) {
.VPHome {
padding-bottom: 128px;
}
}
</style>

View File

@ -0,0 +1,14 @@
<script setup lang="ts">
import { useData } from 'vitepress'
import VPFeatures from './VPFeatures.vue'
const { frontmatter: fm } = useData()
</script>
<template>
<VPFeatures
v-if="fm.features"
class="VPHomeFeatures"
:features="fm.features"
/>
</template>

View File

@ -0,0 +1,18 @@
<script setup lang="ts">
import { useData } from 'vitepress'
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>

View File

@ -0,0 +1,93 @@
<script setup lang="ts">
import VPIconHeart from './icons/VPIconHeart.vue'
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
}
defineProps<{
message?: string
actionText?: string
actionLink?: string
data: Sponsors[]
}>()
</script>
<template>
<section class="VPHomeSponsors">
<div class="container">
<div class="header">
<div class="love"><VPIconHeart class="icon" /></div>
<h2 v-if="message" class="message">{{ message }}</h2>
</div>
<div class="sponsors">
<VPSponsors :data="data" />
</div>
<div v-if="actionLink" class="action">
<VPButton
theme="sponsor"
:text="actionText ?? 'Become a sponsor'"
:href="actionLink"
/>
</div>
</div>
</section>
</template>
<style scoped>
.VPHomeSponsors {
border-top: 1px solid var(--vp-c-divider-light);
padding: 88px 24px 96px;
background-color: var(--vp-c-bg);
}
.container {
margin: 0 auto;
max-width: 1152px;
}
.love {
margin: 0 auto;
width: 28px;
height: 28px;
color: var(--vp-c-text-3);
}
.icon {
width: 28px;
height: 28px;
fill: currentColor;
}
.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>

View File

@ -0,0 +1,39 @@
<script setup lang="ts">
import type { DefaultTheme } from 'vitepress/theme'
import { withBase } from 'vitepress'
defineProps<{
image: DefaultTheme.ThemeableImage
}>()
</script>
<script lang="ts">
export default {
inheritAttrs: false
}
</script>
<template>
<template v-if="image">
<img
v-if="typeof image === 'string' || 'src' in image"
class="VPImage"
v-bind="typeof image === 'string' ? $attrs : { ...image, ...$attrs }"
:src="withBase(typeof image === 'string' ? image : image.src)"
:alt="typeof image === 'string' ? '' : (image.alt || '')"
/>
<template v-else>
<VPImage class="dark" :image="image.dark" v-bind="$attrs" />
<VPImage class="light" :image="image.light" v-bind="$attrs" />
</template>
</template>
</template>
<style scoped>
html:not(.dark) .VPImage.dark {
display: none;
}
.dark .VPImage.light {
display: none;
}
</style>

View File

@ -0,0 +1,39 @@
<script lang="ts" setup>
import { computed } from 'vue'
import { normalizeLink } from '../support/utils.ts'
import VPIconExternalLink from './icons/VPIconExternalLink.vue'
import { EXTERNAL_URL_RE } from '../shared/shared.ts'
const props = defineProps<{
href?: string
noIcon?: boolean
}>()
const isExternal = computed(() => props.href && EXTERNAL_URL_RE.test(props.href))
</script>
<template>
<component
:is="href ? 'a' : 'span'"
class="VPLink"
:class="{ link: href }"
:href="href ? normalizeLink(href) : undefined"
:target="isExternal ? '_blank' : undefined"
:rel="isExternal ? 'noreferrer' : undefined"
>
<slot />
<VPIconExternalLink v-if="isExternal && !noIcon" class="icon" />
</component>
</template>
<style scoped>
.icon {
display: inline-block;
margin-top: -1px;
margin-left: 4px;
width: 11px;
height: 11px;
fill: var(--vp-c-text-3);
transition: fill 0.25s;
}
</style>

View File

@ -0,0 +1,108 @@
<script lang="ts" setup>
import { useSidebar } from '../composables/sidebar.js'
import VPIconAlignLeft from './icons/VPIconAlignLeft.vue'
defineProps<{
open: boolean
}>()
defineEmits<{
(e: 'open-menu'): void
}>()
const { hasSidebar } = useSidebar()
function scrollToTop() {
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
}
</script>
<template>
<div v-if="hasSidebar" class="VPLocalNav">
<button
class="menu"
:aria-expanded="open"
aria-controls="VPSidebarNav"
@click="$emit('open-menu')"
>
<VPIconAlignLeft class="menu-icon" />
<span class="menu-text">Menu</span>
</button>
<a class="top-link" href="#" @click="scrollToTop">
Return to top
</a>
</div>
</template>
<style scoped>
.VPLocalNav {
position: sticky;
top: 0;
left: 0;
z-index: var(--vp-z-index-local-nav);
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--vp-c-divider-light);
width: 100%;
background-color: var(--vp-c-bg);
transition: border-color 0.5s, background-color 0.5s;
}
@media (min-width: 960px) {
.VPLocalNav {
display: none;
}
}
.menu {
display: flex;
align-items: center;
padding: 12px 24px 11px;
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: 768px) {
.menu {
padding: 0 32px;
}
}
.menu-icon {
margin-right: 8px;
width: 16px;
height: 16px;
fill: currentColor;
}
.top-link {
display: block;
padding: 12px 24px 11px;
line-height: 24px;
font-size: 12px;
font-weight: 500;
color: var(--vp-c-text-2);
transition: color 0.5s;
}
.top-link:hover {
color: var(--vp-c-text-1);
transition: color 0.25s;
}
@media (min-width: 768px) {
.top-link {
padding: 12px 32px 11px;
}
}
</style>

View File

@ -0,0 +1,82 @@
<script lang="ts" setup>
import VPMenuLink from './VPMenuLink.vue'
import VPMenuGroup from './VPMenuGroup.vue'
defineProps<{
items?: any[]
}>()
</script>
<template>
<div class="VPMenu">
<div v-if="items" class="items">
<template v-for="item in items" :key="item.text">
<VPMenuLink v-if="'link' in item" :item="item" />
<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-light);
background-color: var(--vp-c-bg);
box-shadow: var(--vp-shadow-3);
transition: background-color 0.5s;
max-height: calc(100vh - var(--vp-nav-height-mobile));
overflow-y: auto;
}
@media (min-width: 960px) {
.VPMenu {
max-height: calc(100vh - var(--vp-nav-height-desktop));
}
}
.dark .VPMenu {
box-shadow: var(--vp-shadow-2);
}
.VPMenu :deep(.group) {
margin: 0 -12px;
padding: 0 12px 12px;
}
.VPMenu :deep(.group + .group) {
border-top: 1px solid var(--vp-c-divider-light);
padding: 11px 12px 12px;
}
.VPMenu :deep(.group:last-child) {
padding-bottom: 0;
}
.VPMenu :deep(.group + .item) {
border-top: 1px solid var(--vp-c-divider-light);
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 .5s;
}
.VPMenu :deep(.action) {
padding-left: 24px;
}
</style>

View File

@ -0,0 +1,46 @@
<script lang="ts" setup>
import VPMenuLink from './VPMenuLink.vue'
defineProps<{
text?: string
items: any[]
}>()
</script>
<template>
<div class="VPMenuGroup">
<p v-if="text" class="title">{{ text }}</p>
<template v-for="item in items">
<VPMenuLink v-if="'link' in item" :item="item" />
</template>
</div>
</template>
<style scoped>
.VPMenuGroup {
margin: 12px -12px 0;
border-top: 1px solid var(--vp-c-divider-light);
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-light);
}
.title {
padding: 0 12px;
line-height: 32px;
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-2);
transition: color 0.25s;
}
</style>

View File

@ -0,0 +1,55 @@
<script lang="ts" setup>
import { useData } from 'vitepress'
import { isActive } from '../support/utils.js'
import VPLink from './VPLink.vue'
defineProps<{
item: any
}>()
const { page } = useData()
</script>
<template>
<div class="VPMenuLink">
<VPLink
:class="{ active: isActive(page.relativePath, item.activeMatch || item.link) }"
:href="item.link"
>
{{ item.text }}
</VPLink>
</div>
</template>
<style scoped>
.VPMenuGroup + .VPMenuLink {
margin: 12px -12px 0;
border-top: 1px solid var(--vp-c-divider-light);
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);
background-color: var(--vp-c-bg-mute);
}
.dark .link:hover {
background-color: var(--vp-c-bg-soft);
}
.link.active {
color: var(--vp-c-brand);
}
</style>

View File

@ -0,0 +1,64 @@
<script setup lang="ts">
import { provide } from 'vue'
import { useNav } from '../composables/nav.js'
import { useSidebar } from '../composables/sidebar.js'
import VPNavBar from './VPNavBar.vue'
import VPNavScreen from './VPNavScreen.vue'
const { isScreenOpen, closeScreen, toggleScreen } = useNav()
const { hasSidebar } = useSidebar()
provide('close-screen', closeScreen)
</script>
<template>
<header class="VPNav" :class="{ 'no-sidebar' : !hasSidebar }">
<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 scoped>
.VPNav {
position: relative;
top: 0;
left: 0;
z-index: var(--vp-z-index-nav);
width: 100%;
pointer-events: none;
}
@media (min-width: 960px) {
.VPNav {
position: fixed;
}
.VPNav.no-sidebar {
-webkit-backdrop-filter: saturate(50%) blur(8px);
backdrop-filter: saturate(50%) blur(8px);
background: rgba(255, 255, 255, 0.7);
}
.dark .VPNav.no-sidebar {
background: rgba(36, 36, 36, 0.7);
}
@supports not (backdrop-filter: saturate(50%) blur(8px)) {
.VPNav.no-sidebar {
background: rgba(255, 255, 255, 0.95);
}
.dark .VPNav.no-sidebar {
background: rgba(36, 36, 36, 0.95);
}
}
}
</style>

View File

@ -0,0 +1,139 @@
<script lang="ts" setup>
import { useSidebar } from '../composables/sidebar.js'
import VPNavBarTitle from './VPNavBarTitle.vue'
import VPNavBarSearch from './VPNavBarSearch.vue'
import VPNavBarMenu from './VPNavBarMenu.vue'
import VPNavBarTranslations from './VPNavBarTranslations.vue'
import VPNavBarAppearance from './VPNavBarAppearance.vue'
import VPNavBarSocialLinks from './VPNavBarSocialLinks.vue'
import VPNavBarExtra from './VPNavBarExtra.vue'
import VPNavBarHamburger from './VPNavBarHamburger.vue'
defineProps<{
isScreenOpen: boolean
}>()
defineEmits<{
(e: 'toggle-screen'): void
}>()
const { hasSidebar } = useSidebar()
</script>
<template>
<div class="VPNavBar" :class="{ 'has-sidebar' : hasSidebar }">
<div class="container">
<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 class="content">
<slot name="nav-bar-content-before" />
<VPNavBarSearch class="search" />
<VPNavBarMenu class="menu" />
<VPNavBarTranslations class="translations" />
<VPNavBarAppearance class="appearance" />
<VPNavBarSocialLinks class="social-links" />
<VPNavBarExtra class="extra" />
<slot name="nav-bar-content-after" />
<VPNavBarHamburger
class="hamburger"
:active="isScreenOpen"
@click="$emit('toggle-screen')"
/>
</div>
</div>
</div>
</template>
<style scoped>
.VPNavBar {
position: relative;
border-bottom: 1px solid var(--vp-c-divider-light);
padding: 0 8px 0 24px;
height: var(--vp-nav-height-mobile);
transition: border-color 0.5s, background-color 0.5s;
pointer-events: none;
}
@media (min-width: 768px) {
.VPNavBar {
padding: 0 32px;
}
}
@media (min-width: 960px) {
.VPNavBar {
height: var(--vp-nav-height-desktop);
border-bottom: 0;
}
.VPNavBar.has-sidebar .content {
margin-right: -32px;
padding-right: 32px;
-webkit-backdrop-filter: saturate(50%) blur(8px);
backdrop-filter: saturate(50%) blur(8px);
background: rgba(255, 255, 255, 0.7);
}
.dark .VPNavBar.has-sidebar .content {
background: rgba(36, 36, 36, 0.7);
}
@supports not (backdrop-filter: saturate(50%) blur(8px)) {
.VPNavBar.has-sidebar .content {
background: rgba(255, 255, 255, 0.95);
}
.dark .VPNavBar.has-sidebar .content {
background: rgba(36, 36, 36, 0.95);
}
}
}
.container {
display: flex;
justify-content: space-between;
margin: 0 auto;
max-width: calc(var(--vp-layout-max-width) - 64px);
pointer-events: none;
}
.container :deep(*) {
pointer-events: auto;
}
.content {
display: flex;
justify-content: flex-end;
align-items: center;
flex-grow: 1;
}
.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-light);
content: "";
}
.menu + .appearance::before,
.translations + .appearance::before {
margin-right: 16px;
}
.appearance + .social-links::before {
margin-left: 16px;
}
.social-links {
margin-right: -8px;
}
</style>

View File

@ -0,0 +1,25 @@
<script lang="ts" setup>
import { useData } from 'vitepress'
import VPSwitchAppearance from './VPSwitchAppearance.vue'
const { site } = useData()
</script>
<template>
<div v-if="site.appearance" class="VPNavBarAppearance">
<VPSwitchAppearance />
</div>
</template>
<style scoped>
.VPNavBarAppearance {
display: none;
}
@media (min-width: 1280px) {
.VPNavBarAppearance {
display: flex;
align-items: center;
}
}
</style>

View File

@ -0,0 +1,85 @@
<script lang="ts" setup>
import { useData } from 'vitepress'
import VPFlyout from './VPFlyout.vue'
import VPMenuLink from './VPMenuLink.vue'
import VPSwitchAppearance from './VPSwitchAppearance.vue'
import VPSocialLinks from './VPSocialLinks.vue'
import { computed } from 'vue'
const { site, theme } = useData()
const hasExtraContent = computed(() => theme.value.localeLinks || site.value.appearance || theme.value.socialLinks)
</script>
<template>
<VPFlyout v-if="hasExtraContent" class="VPNavBarExtra" label="extra navigation">
<div v-if="theme.localeLinks" class="group">
<p class="trans-title">{{ theme.localeLinks.text }}</p>
<template v-for="locale in theme.localeLinks.items" :key="locale.link">
<VPMenuLink :item="locale" />
</template>
</div>
<div v-if="site.appearance" class="group">
<div class="item appearance">
<p class="label">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>

View File

@ -0,0 +1,79 @@
<script lang="ts" setup>
defineProps<{
active: boolean
}>()
defineEmits<{
(e: 'click'): void
}>()
</script>
<template>
<button
type="button"
class="VPNavBarHamburger"
:class="{ active }"
aria-label="mobile navigation"
:aria-expanded="active"
aria-controls="VPNavScreen"
@click="$emit('click')"
>
<span class="container">
<span class="top" />
<span class="middle" />
<span class="bottom" />
</span>
</button>
</template>
<style scoped>
.VPNavBarHamburger {
display: flex;
justify-content: center;
align-items: center;
width: 48px;
height: var(--vp-nav-height);
}
@media (min-width: 768px) {
.VPNavBarHamburger {
display: none;
}
}
.container {
position: relative;
width: 16px;
height: 14px;
overflow: hidden;
}
.VPNavBarHamburger:hover .top { top: 0; left: 0; transform: translateX(4px); }
.VPNavBarHamburger:hover .middle { top: 6px; left: 0; transform: translateX(0); }
.VPNavBarHamburger:hover .bottom { top: 12px; left: 0; transform: translateX(8px); }
.VPNavBarHamburger.active .top { top: 6px; transform: translateX(0) rotate(225deg); }
.VPNavBarHamburger.active .middle { top: 6px; transform: translateX(16px); }
.VPNavBarHamburger.active .bottom { top: 6px; transform: translateX(0) rotate(135deg); }
.VPNavBarHamburger.active:hover .top,
.VPNavBarHamburger.active:hover .middle,
.VPNavBarHamburger.active:hover .bottom {
background-color: var(--vp-c-text-2);
transition: top .25s, background-color .25s, transform .25s;
}
.top,
.middle,
.bottom {
position: absolute;
width: 16px;
height: 2px;
background-color: var(--vp-c-text-1);
transition: top .25s, background-color .5s, transform .25s;
}
.top { top: 0; left: 0; transform: translateX(0); }
.middle { top: 6px; left: 0; transform: translateX(8px); }
.bottom { top: 12px; left: 0; transform: translateX(4px); }
</style>

View File

@ -0,0 +1,29 @@
<script lang="ts" setup>
import { useData } from 'vitepress'
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="item.text">
<VPNavBarMenuLink v-if="'link' in item" :item="item" />
<VPNavBarMenuGroup v-else :item="item" />
</template>
</nav>
</template>
<style scoped>
.VPNavBarMenu {
display: none;
}
@media (min-width: 768px) {
.VPNavBarMenu {
display: flex;
}
}
</style>

View File

@ -0,0 +1,27 @@
<script lang="ts" setup>
import { useData } from 'vitepress'
import type { DefaultTheme } from 'vitepress/theme'
import { isActive } from '../support/utils.js'
import VPFlyout from './VPFlyout.vue'
defineProps<{
item: DefaultTheme.NavItemWithChildren
}>()
const { page } = useData()
</script>
<template>
<VPFlyout
:class="{
VPNavBarMenuGroup: true,
active: isActive(
page.relativePath,
item.activeMatch,
!!item.activeMatch
)
}"
:button="item.text"
:items="item.items"
/>
</template>

View File

@ -0,0 +1,56 @@
<script lang="ts" setup>
import type { DefaultTheme } from 'vitepress/theme'
import { useData } from 'vitepress'
import { isActive } from '../support/utils.js'
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"
:noIcon="true"
>
{{ item.text }}
</VPLink>
</template>
<style scoped>
.VPNavBarMenuLink {
display: flex;
align-items: center;
padding: 0 12px;
line-height: var(--vp-nav-height-mobile);
font-size: 14px;
font-weight: 500;
color: var(--vp-c-text-1);
transition: color 0.25s;
}
.VPNavBarMenuLink.active {
color: var(--vp-c-brand);
}
.VPNavBarMenuLink:hover {
color: var(--vp-c-brand);
}
@media (min-width: 1280px) {
.VPNavBarMenuLink {
line-height: var(--vp-nav-height-desktop);
}
}
</style>

View File

@ -0,0 +1,287 @@
<script lang="ts" setup>
import '@docsearch/css'
import { defineAsyncComponent, ref, onMounted, onUnmounted } from 'vue'
import { useData } from 'vitepress'
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 metaKey = ref(`'Meta'`)
onMounted(() => {
if (!theme.value.algolia) {
return
}
// meta key detect (same logic as in @docsearch/js)
metaKey.value = /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform)
? `'⌘'`
: `'Ctrl'`
const handleSearchHotKey = (e: KeyboardEvent) => {
if (e.key === 'k' && (e.ctrlKey || e.metaKey)) {
e.preventDefault()
load()
remove()
}
}
const remove = () => {
window.removeEventListener('keydown', handleSearchHotKey)
}
window.addEventListener('keydown', handleSearchHotKey)
onUnmounted(remove)
})
function load() {
if (!loaded.value) {
loaded.value = true
}
}
</script>
<template>
<div v-if="theme.algolia" class="VPNavBarSearch">
<VPAlgoliaSearchBox v-if="loaded" />
<div v-else id="docsearch" @click="load">
<button
type="button"
class="DocSearch DocSearch-Button"
aria-label="Search"
>
<span class="DocSearch-Button-Container">
<svg
class="DocSearch-Search-Icon"
width="20"
height="20"
viewBox="0 0 20 20"
>
<path
d="M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z"
stroke="currentColor"
fill="none"
fill-rule="evenodd"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
<span class="DocSearch-Button-Placeholder">{{ theme.algolia?.buttonText || 'Search' }}</span>
</span>
<span class="DocSearch-Button-Keys">
<kbd class="DocSearch-Button-Key"></kbd>
<kbd class="DocSearch-Button-Key">K</kbd>
</span>
</button>
</div>
</div>
</template>
<style>
.VPNavBarSearch {
display: flex;
align-items: center;
}
@media (min-width: 768px) {
.VPNavBarSearch {
flex-grow: 1;
padding-left: 24px;
}
}
@media (min-width: 960px) {
.VPNavBarSearch {
padding-left: 32px;
}
}
.DocSearch {
--docsearch-primary-color: var(--vp-c-brand);
--docsearch-highlight-color: var(--docsearch-primary-color);
--docsearch-text-color: var(--vp-c-text-1);
--docsearch-muted-color: var(--vp-c-text-2);
--docsearch-searchbox-shadow: none;
--docsearch-searchbox-focus-background: transparent;
--docsearch-key-gradient: transparent;
--docsearch-key-shadow: none;
--docsearch-modal-background: var(--vp-c-bg-soft);
--docsearch-footer-background: var(--vp-c-bg);
}
.dark .DocSearch {
--docsearch-modal-shadow: none;
--docsearch-footer-shadow: none;
--docsearch-logo-color: var(--vp-c-text-2);
--docsearch-hit-background: var(--vp-c-bg-mute);
--docsearch-hit-color: var(--vp-c-text-2);
--docsearch-hit-shadow: none;
}
.DocSearch-Button {
display: flex;
justify-content: center;
align-items: center;
margin: 0;
padding: 0;
width: 32px;
height: 55px;
background: transparent;
transition: border-color 0.25s;
}
.DocSearch-Button:hover {
background: transparent;
}
.DocSearch-Button:focus {
outline: 1px dotted;
outline: 5px auto -webkit-focus-ring-color;
}
.DocSearch-Button:focus:not(:focus-visible) {
outline: none !important;
}
@media (min-width: 768px) {
.DocSearch-Button {
justify-content: flex-start;
border: 1px solid transparent;
border-radius: 8px;
padding: 0 10px 0 12px;
width: 100%;
height: 40px;
background-color: var(--vp-c-bg-alt);
}
.DocSearch-Button:hover {
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-alt);
}
}
.DocSearch-Button .DocSearch-Button-Container {
display: flex;
align-items: center;
}
.DocSearch-Button .DocSearch-Search-Icon {
position: relative;
width: 16px;
height: 16px;
color: var(--vp-c-text-1);
fill: currentColor;
transition: color 0.5s;
}
.DocSearch-Button:hover .DocSearch-Search-Icon {
color: var(--vp-c-text-1);
}
@media (min-width: 768px) {
.DocSearch-Button .DocSearch-Search-Icon {
top: 1px;
margin-right: 8px;
width: 14px;
height: 14px;
color: var(--vp-c-text-2);
}
}
.DocSearch-Button .DocSearch-Button-Placeholder {
display: none;
margin-top: 2px;
padding: 0 16px 0 0;
font-size: 13px;
font-weight: 500;
color: var(--vp-c-text-2);
transition: color 0.5s;
}
.DocSearch-Button:hover .DocSearch-Button-Placeholder {
color: var(--vp-c-text-1);
}
@media (min-width: 768px) {
.DocSearch-Button .DocSearch-Button-Placeholder {
display: inline-block;
}
}
.DocSearch-Button .DocSearch-Button-Keys {
display: none;
min-width: auto;
}
@media (min-width: 768px) {
.DocSearch-Button .DocSearch-Button-Keys {
display: flex;
align-items: center;
}
}
.DocSearch-Button .DocSearch-Button-Key {
display: block;
margin: 2px 0 0 0;
border: 1px solid var(--vp-c-divider);
border-right: none;
border-radius: 4px 0 0 4px;
padding-left: 6px;
min-width: 0;
width: auto;
height: 22px;
line-height: 22px;
font-family: var(--vp-font-family-base);
font-size: 12px;
font-weight: 500;
transition: color 0.5s, border-color 0.5s;
}
.DocSearch-Button .DocSearch-Button-Key + .DocSearch-Button-Key {
border-right: 1px solid var(--vp-c-divider);
border-left: none;
border-radius: 0 4px 4px 0;
padding-left: 2px;
padding-right: 6px;
}
.DocSearch-Button .DocSearch-Button-Key:first-child {
font-size: 1px;
letter-spacing: -12px;
color: transparent;
}
.DocSearch-Button .DocSearch-Button-Key:first-child:after {
content: v-bind(metaKey);
font-size: 12px;
letter-spacing: normal;
color: var(--docsearch-muted-color);
}
.DocSearch-Button .DocSearch-Button-Key:first-child > * {
display: none;
}
.dark .DocSearch-Footer {
border-top: 1px solid var(--vp-c-divider);
}
.DocSearch-Form {
border: 1px solid var(--vp-c-brand);
background-color: var(--vp-c-white);
}
.dark .DocSearch-Form {
background-color: var(--vp-c-bg-mute);
}
</style>

View File

@ -0,0 +1,27 @@
<script lang="ts" setup>
import { useData } from 'vitepress'
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>

View File

@ -0,0 +1,62 @@
<script setup lang="ts">
import { useData } from 'vitepress'
import { useSidebar } from '../composables/sidebar.js'
import VPImage from './VPImage.vue'
const { site, theme } = useData()
const { hasSidebar } = useSidebar()
</script>
<template>
<div class="VPNavBarTitle" :class="{ 'has-sidebar': hasSidebar }">
<a class="title" :href="site.base">
<slot name="nav-bar-title-before" />
<VPImage class="logo" :image="theme.logo" />
<template v-if="theme.siteTitle">{{ theme.siteTitle }}</template>
<template v-else-if="theme.siteTitle === undefined">{{ site.title }}</template>
<slot name="nav-bar-title-after" />
</a>
</div>
</template>
<style scoped>
.VPNavBarTitle {
flex-shrink: 0;
border-bottom: 1px solid transparent;
}
@media (min-width: 960px) {
.VPNavBarTitle.has-sidebar {
margin-right: 32px;
width: calc(var(--vp-sidebar-width) - 64px);
border-bottom-color: var(--vp-c-divider-light);
background-color: var(--vp-c-bg-alt);
}
}
.title {
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;
}
.title:hover {
opacity: 0.6;
}
@media (min-width: 960px) {
.title {
flex-shrink: 0;
}
}
:deep(.logo) {
margin-right: 8px;
height: 24px;
}
</style>

View File

@ -0,0 +1,45 @@
<script lang="ts" setup>
import { useData } from 'vitepress'
import VPIconLanguages from './icons/VPIconLanguages.vue'
import VPFlyout from './VPFlyout.vue'
import VPMenuLink from './VPMenuLink.vue'
const { theme } = useData()
</script>
<template>
<VPFlyout
v-if="theme.localeLinks"
class="VPNavBarTranslations"
:icon="VPIconLanguages"
>
<div class="items">
<p class="title">{{ theme.localeLinks.text }}</p>
<template v-for="locale in theme.localeLinks.items" :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>

View File

@ -0,0 +1,103 @@
<script setup lang="ts">
import { ref } from 'vue'
import { disableBodyScroll, clearAllBodyScrollLocks } from 'body-scroll-lock'
import VPNavScreenMenu from './VPNavScreenMenu.vue'
import VPNavScreenAppearance from './VPNavScreenAppearance.vue'
import VPNavScreenTranslations from './VPNavScreenTranslations.vue'
import VPNavScreenSocialLinks from './VPNavScreenSocialLinks.vue'
defineProps<{
open: boolean
}>()
const screen = ref<HTMLElement | null>(null)
function lockBodyScroll() {
disableBodyScroll(screen.value!, { reserveScrollBarGap: true })
}
function unlockBodyScroll() {
clearAllBodyScrollLocks()
}
</script>
<template>
<transition
name="fade"
@enter="lockBodyScroll"
@after-leave="unlockBodyScroll"
>
<div v-if="open" class="VPNavScreen" ref="screen">
<div class="container">
<slot name="nav-screen-content-before" />
<VPNavScreenMenu class="menu" />
<VPNavScreenTranslations class="translations" />
<VPNavScreenAppearance class="appearance" />
<VPNavScreenSocialLinks class="social-links" />
<slot name="nav-screen-content-after" />
</div>
</div>
</transition>
</template>
<style scoped>
.VPNavScreen {
position: fixed;
top: var(--vp-nav-height-mobile);
right: 0;
bottom: 0;
left: 0;
padding: 0 32px;
width: 100%;
background-color: var(--vp-c-bg);
overflow-y: auto;
transition: background-color 0.5s;
pointer-events: auto;
}
.VPNavScreen.fade-enter-active,
.VPNavScreen.fade-leave-active {
transition: opacity 0.25s;
}
.VPNavScreen.fade-enter-active .container,
.VPNavScreen.fade-leave-active .container {
transition: transform 0.25s ease;
}
.VPNavScreen.fade-enter-from,
.VPNavScreen.fade-leave-to {
opacity: 0;
}
.VPNavScreen.fade-enter-from .container,
.VPNavScreen.fade-leave-to .container {
transform: translateY(-8px);
}
@media (min-width: 768px) {
.VPNavScreen {
display: none;
}
}
.container {
margin: 0 auto;
padding: 24px 0 96px;
max-width: 288px;
}
.menu + .translations,
.menu + .appearance,
.translations + .appearance {
margin-top: 24px;
}
.menu + .social-links {
margin-top: 16px;
}
.appearance + .social-links {
margin-top: 16px;
}
</style>

View File

@ -0,0 +1,33 @@
<script lang="ts" setup>
import { useData } from 'vitepress'
import VPSwitchAppearance from './VPSwitchAppearance.vue'
const { site } = useData()
</script>
<template>
<div v-if="site.appearance" class="VPNavScreenAppearance">
<p class="text">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);
transition: background-color 0.5s;
}
.text {
line-height: 24px;
font-size: 12px;
font-weight: 500;
color: var(--vp-c-text-2);
transition: color 0.5s;
}
</style>

View File

@ -0,0 +1,24 @@
<script lang="ts" setup>
import { useData } from 'vitepress'
import VPNavScreenMenuLink from './VPNavScreenMenuLink.vue'
import VPNavScreenMenuGroup from './VPNavScreenMenuGroup.vue'
const { theme } = useData()
</script>
<template>
<nav v-if="theme.nav" class="VPNavScreenMenu">
<template v-for="item in theme.nav" :key="item.text">
<VPNavScreenMenuLink
v-if="'link' in item"
:text="item.text"
:link="item.link"
/>
<VPNavScreenMenuGroup
v-else
:text="item.text || ''"
:items="item.items"
/>
</template>
</nav>
</template>

View File

@ -0,0 +1,117 @@
<script lang="ts" setup>
import { computed, ref } from 'vue'
import VPIconPlus from './icons/VPIconPlus.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="VPNavScreenMenuGroup" :class="{ open: isOpen }">
<button
class="button"
:aria-controls="groupId"
:aria-expanded="isOpen"
@click="toggle"
>
<span class="button-text">{{ text }}</span>
<VPIconPlus class="button-icon" />
</button>
<div :id="groupId" class="items">
<template v-for="item in items" :key="item.text">
<div v-if="'link' in item" :key="item.text" class="item">
<VPNavScreenMenuGroupLink
:text="item.text"
:link="item.link"
/>
</div>
<div v-else class="group">
<VPNavScreenMenuGroupSection
:text="item.text"
:items="item.items"
/>
</div>
</template>
</div>
</div>
</template>
<style scoped>
.VPNavScreenMenuGroup {
border-bottom: 1px solid var(--vp-c-divider-light);
height: 48px;
overflow: hidden;
transition: border-color 0.5s;
}
.VPNavScreenMenuGroup .items {
visibility: hidden;
}
.VPNavScreenMenuGroup.open .items {
visibility: visible;
}
.VPNavScreenMenuGroup.open {
padding-bottom: 10px;
height: auto;
}
.VPNavScreenMenuGroup.open .button {
padding-bottom: 6px;
color: var(--vp-c-brand);
}
.VPNavScreenMenuGroup.open .button-icon {
transform: rotate(45deg);
}
.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;
}
.button:hover {
color: var(--vp-c-brand);
}
.button-icon {
width: 14px;
height: 14px;
fill: var(--vp-c-text-2);
transition: fill 0.5s, transform 0.25s;
}
.group:first-child {
padding-top: 0px;
}
.group + .group,
.group + .item {
padding-top: 4px;
}
</style>

View File

@ -0,0 +1,33 @@
<script lang="ts" setup>
import { inject } from 'vue'
import VPLink from './VPLink.vue'
defineProps<{
text: string
link: string
}>()
const closeScreen = inject('close-screen') as () => void
</script>
<template>
<VPLink class="VPNavScreenMenuGroupLink" :href="link" @click="closeScreen">
{{ text }}
</VPLink>
</template>
<style scoped>
.VPNavScreenMenuGroupLink {
display: block;
line-height: 32px;
font-size: 13px;
font-weight: 400;
color: var(--vp-c-text-1);
transition: color 0.25s;
margin-left: 12px;
}
.VPNavScreenMenuGroupLink:hover {
color: var(--vp-c-brand);
}
</style>

View File

@ -0,0 +1,35 @@
<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="VPNavScreenMenuGroupSection">
<p v-if="text" class="title">{{ text }}</p>
<VPNavScreenMenuGroupLink
v-for="item in items"
:key="item.text"
:text="item.text"
:link="item.link"
/>
</div>
</template>
<style scoped>
.VPNavScreenMenuGroupSection {
display: block;
}
.title {
line-height: 32px;
font-size: 13px;
font-weight: 700;
color: var(--vp-c-text-2);
transition: color 0.25s;
}
</style>

View File

@ -0,0 +1,34 @@
<script lang="ts" setup>
import { inject } from 'vue'
import VPLink from './VPLink.vue'
defineProps<{
text: string
link: string
}>()
const closeScreen = inject('close-screen') as () => void
</script>
<template>
<VPLink class="VPNavScreenMenuLink" :href="link" @click="closeScreen">
{{ text }}
</VPLink>
</template>
<style scoped>
.VPNavScreenMenuLink {
display: block;
border-bottom: 1px solid var(--vp-c-divider-light);
padding: 12px 0 11px;
line-height: 24px;
font-size: 14px;
font-weight: 500;
color: var(--vp-c-text-1);
transition: border-color 0.5s, color 0.25s;
}
.VPNavScreenMenuLink:hover {
color: var(--vp-c-brand);
}
</style>

View File

@ -0,0 +1,14 @@
<script lang="ts" setup>
import { useData } from 'vitepress'
import VPSocialLinks from './VPSocialLinks.vue'
const { theme } = useData()
</script>
<template>
<VPSocialLinks
v-if="theme.socialLinks"
class="VPNavScreenSocialLinks"
:links="theme.socialLinks"
/>
</template>

View File

@ -0,0 +1,73 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useData } from 'vitepress'
import VPIconChevronDown from './icons/VPIconChevronDown.vue'
import VPIconLanguages from './icons/VPIconLanguages.vue'
const { theme } = useData()
const isOpen = ref(false)
function toggle() {
isOpen.value = !isOpen.value
}
</script>
<template>
<div v-if="theme.localeLinks" class="VPNavScreenTranslations" :class="{ open: isOpen }">
<button class="title" @click="toggle">
<VPIconLanguages class="icon lang" />
{{ theme.localeLinks.text }}
<VPIconChevronDown class="icon chevron" />
</button>
<ul class="list">
<li v-for="locale in theme.localeLinks.items" :key="locale.link" class="item">
<a class="link" :href="locale.link">{{ locale.text }}</a>
</li>
</ul>
</div>
</template>
<style scoped>
.VPNavScreenTranslations {
height: 24px;
overflow: hidden;
}
.VPNavScreenTranslations.open {
height: auto;
}
.title {
display: flex;
align-items: center;
font-size: 14px;
font-weight: 500;
color: var(--vp-c-text-1);
}
.icon {
width: 16px;
height: 16px;
fill: currentColor;
}
.icon.lang {
margin-right: 8px;
}
.icon.chevron {
margin-left: 4px;
}
.list {
padding: 4px 0 0 24px;
}
.link {
line-height: 32px;
font-size: 13px;
color: var(--vp-c-text-1);
}
</style>

View File

@ -0,0 +1,5 @@
<template>
<div class="VPPage">
<Content />
</div>
</template>

View File

@ -0,0 +1,121 @@
<script lang="ts" setup>
import { ref, watchPostEffect, nextTick } from 'vue'
import { useSidebar } from '../composables/sidebar.js'
import VPSidebarGroup from './VPSidebarGroup.vue'
const { sidebar, hasSidebar } = useSidebar()
const props = defineProps<{
open: boolean
}>()
// a11y: focus Nav element when menu has opened
let navEl = ref<(Element & { focus(): void }) | null>(null)
watchPostEffect(async () => {
if (props.open) {
await nextTick()
navEl.value?.focus()
}
})
</script>
<template>
<aside
v-if="hasSidebar"
class="VPSidebar"
:class="{ open }"
ref="navEl"
@click.stop
>
<nav class="nav" id="VPSidebarNav" aria-labelledby="sidebar-aria-label" tabindex="-1">
<span class="visually-hidden" id="sidebar-aria-label">
Sidebar Navigation
</span>
<div v-for="group in sidebar" :key="group.text" class="group">
<VPSidebarGroup
:text="group.text"
:items="group.items"
:collapsible="group.collapsible"
:collapsed="group.collapsed"
/>
</div>
</nav>
</aside>
</template>
<style scoped>
.VPSidebar {
position: fixed;
top: 0;
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-c-bg);
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;
}
.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 {
z-index: 1;
padding-top: var(--vp-nav-height-desktop);
padding-bottom: 128px;
width: var(--vp-sidebar-width);
max-width: 100%;
background-color: var(--vp-c-bg-alt);
opacity: 1;
visibility: visible;
box-shadow: none;
transform: translateX(0);
}
}
@media (min-width: 1440px) {
.VPSidebar {
padding-left: max(32px, calc((100% - (var(--vp-layout-max-width) - 64px)) / 2));
width: calc((100% - (var(--vp-layout-max-width) - 64px)) / 2 + var(--vp-sidebar-width) - 32px);
}
}
.nav {
outline: 0;
}
.group + .group {
margin-top: 32px;
border-top: 1px solid var(--vp-c-divider-light);
padding-top: 10px;
}
@media (min-width: 960px) {
.group {
padding-top: 10px;
width: calc(var(--vp-sidebar-width) - 64px);
}
.group + .group {
margin-top: 24px;
}
}
</style>

View File

@ -0,0 +1,128 @@
<script lang="ts" setup>
import type { DefaultTheme } from 'vitepress/theme'
import { ref, watchEffect } from 'vue'
import { useData } from 'vitepress'
import { isActive } from '../support/utils.js'
import VPIconPlusSquare from './icons/VPIconPlusSquare.vue'
import VPIconMinusSquare from './icons/VPIconMinusSquare.vue'
import VPSidebarLink from './VPSidebarLink.vue'
const props = defineProps<{
text?: string
items: DefaultTheme.SidebarItem[]
collapsible?: boolean
collapsed?: boolean
}>()
const collapsed = ref(false)
watchEffect(() => {
collapsed.value = !!(props.collapsible && props.collapsed)
})
const { page } = useData()
watchEffect(() => {
if(props.items.some((item) => { return isActive(page.value.relativePath, item.link) })){
collapsed.value = false
}
})
function toggle() {
if (props.collapsible) {
collapsed.value = !collapsed.value
}
}
</script>
<template>
<section class="VPSidebarGroup" :class="{ collapsible, collapsed }">
<div
v-if="text"
class="title"
:role="collapsible ? 'button' : undefined"
@click="toggle"
>
<h2 class="title-text">{{ text }}</h2>
<div class="action">
<VPIconMinusSquare class="icon minus" />
<VPIconPlusSquare class="icon plus" />
</div>
</div>
<div class="items">
<template v-for="item in items" :key="item.link">
<VPSidebarLink :item="item" />
</template>
</div>
</section>
</template>
<style scoped>
.title {
display: flex;
justify-content: space-between;
align-items: flex-start;
z-index: 2;
}
.title-text {
padding-top: 6px;
padding-bottom: 6px;
line-height: 20px;
font-size: 14px;
font-weight: 700;
color: var(--vp-c-text-1);
}
.action {
display: none;
position: relative;
margin-right: -8px;
border-radius: 4px;
width: 32px;
height: 32px;
color: var(--vp-c-text-3);
transition: color 0.25s;
}
.VPSidebarGroup.collapsible .action {
display: block;
}
.VPSidebarGroup.collapsible .title {
cursor: pointer;
}
.title:hover .action {
color: var(--vp-c-text-2);
}
.icon {
position: absolute;
top: 8px;
left: 8px;
width: 16px;
height: 16px;
fill: currentColor;
}
.icon.minus { opacity: 1; }
.icon.plus { opacity: 0; }
.VPSidebarGroup.collapsed .icon.minus { opacity: 0; }
.VPSidebarGroup.collapsed .icon.plus { opacity: 1; }
.items {
overflow: hidden;
}
.VPSidebarGroup.collapsed .items {
margin-bottom: -22px;
max-height: 0;
}
@media (min-width: 960px) {
.VPSidebarGroup.collapsed .items {
margin-bottom: -14px;
}
}
</style>

View File

@ -0,0 +1,71 @@
<script lang="ts" setup>
import type { DefaultTheme } from 'vitepress/theme'
import { computed, inject } from 'vue'
import { useData } from 'vitepress'
import { isActive } from '../support/utils.js'
import VPLink from './VPLink.vue'
withDefaults(
defineProps<{ item: DefaultTheme.SidebarItem; depth?: number }>(),
{ depth: 1 }
)
const { page, frontmatter } = useData()
const maxDepth = computed<number>(
() => frontmatter.value.sidebarDepth || Infinity
)
const closeSideBar = inject('close-sidebar') as () => void
</script>
<template>
<VPLink
class="link"
:class="{ active: isActive(page.relativePath, item.link) }"
:style="{ paddingLeft: 16 * (depth - 1) + 'px' }"
:href="item.link"
@click="closeSideBar"
>
<span class="link-text" :class="{ light: depth > 1 }">{{ item.text }}</span>
</VPLink>
<template
v-if="'items' in item && depth < maxDepth"
v-for="child in item.items"
:key="child.link"
>
<VPSidebarLink :item="child" :depth="depth + 1" />
</template>
</template>
<style scoped>
.link {
display: block;
margin: 4px 0;
color: var(--vp-c-text-2);
transition: color 0.5s;
}
.link:hover {
color: var(--vp-c-text-1);
}
.link.active {
color: var(--vp-c-brand);
}
.link :deep(.icon) {
width: 12px;
height: 12px;
fill: currentColor;
}
.link-text {
line-height: 20px;
font-size: 14px;
font-weight: 500;
}
.link-text.light {
font-size: 13px;
font-weight: 400;
}
</style>

View File

@ -0,0 +1,72 @@
<script lang="ts" setup>
import { ref, watch } from 'vue'
import { useRoute } from 'vitepress'
const route = useRoute()
const backToTop = ref()
watch(() => route.path, () => backToTop.value.focus())
function focusOnTargetAnchor({ target }: Event) {
const el = document.querySelector<HTMLAnchorElement>(
(target as HTMLAnchorElement).hash
)
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"
>
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);
box-shadow: var(--vp-shadow-3);
background-color: var(--vp-c-bg);
}
.VPSkipLink:focus {
height: auto;
width: auto;
clip: auto;
clip-path: none;
}
.dark .VPSkipLink {
color: var(--vp-c-green);
}
@media (min-width: 1280px) {
.VPSkipLink {
top: 14px;
left: 16px;
}
}
</style>

View File

@ -0,0 +1,49 @@
<script lang="ts" setup>
import type { DefaultTheme } from 'vitepress/theme'
import { computed } from 'vue'
import { icons } from '../support/socialIcons.js'
const props = defineProps<{
icon: DefaultTheme.SocialLinkIcon
link: string
}>()
const svg = computed(() => {
if (typeof props.icon === 'object') return props.icon.svg
return icons[props.icon]
})
</script>
<template>
<a
class="VPSocialLink"
:href="link"
target="_blank"
rel="noopener"
v-html="svg"
>
</a>
</template>
<style scoped>
.VPSocialLink {
display: flex;
justify-content: center;
align-items: center;
width: 36px;
height: 36px;
color: var(--vp-c-text-2);
transition: color 0.5s;
}
.VPSocialLink:hover {
color: var(--vp-c-text-1);
transition: color 0.25s;
}
.VPSocialLink > :deep(svg) {
width: 20px;
height: 20px;
fill: currentColor;
}
</style>

View File

@ -0,0 +1,27 @@
<script lang="ts" setup>
import type { DefaultTheme } from 'vitepress/theme'
import VPSocialLink from './VPSocialLink.vue'
defineProps<{
links: DefaultTheme.SocialLink[]
}>()
</script>
<template>
<div class="VPSocialLinks">
<VPSocialLink
v-for="{ link, icon } in links"
:key="link"
:icon="icon"
:link="link"
/>
</div>
</template>
<style scoped>
.VPSocialLinks {
display: flex;
flex-wrap: wrap;
justify-content: center;
}
</style>

View File

@ -0,0 +1,46 @@
<script setup lang="ts">
import type { GridSize } from '../composables/sponsor-grid.js'
import type { Sponsor } from './VPSponsorsGrid.vue'
import { computed } from 'vue'
import VPSponsorsGrid from './VPSponsorsGrid.vue'
export interface Sponsors {
tier?: string
size?: GridSize
items: Sponsor[]
}
const props = defineProps<{
mode?: 'normal' | 'aside'
tier?: string
size?: GridSize
data: Sponsors[] | Sponsor[]
}>()
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 ?? 'normal']">
<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>

View File

@ -0,0 +1,50 @@
<script setup lang="ts">
import type { GridSize } from '../composables/sponsor-grid.js'
import { ref } from 'vue'
import { useSponsorsGrid } from '../composables/sponsor-grid.js'
export interface Sponsor {
name: string
img: string
url: string
}
const props = defineProps<{
size?: GridSize
data: Sponsor[]
}>()
const el = ref(null)
useSponsorsGrid({ el, size: props.size })
</script>
<template>
<div
class="VPSponsorsGrid vp-sponsor-grid"
:class="[props.size ?? 'medium']"
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">
<h4 class="visually-hidden">{{ sponsor.name }}</h4>
<img
class="vp-sponsor-grid-image"
:src="sponsor.img"
:alt="sponsor.name"
/>
</article>
</a>
</div>
</div>
</template>

View File

@ -0,0 +1,66 @@
<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-c-divider);
background-color: var(--vp-c-bg-mute);
transition: border-color 0.25s, background-color 0.25s;
}
.VPSwitch:hover {
border-color: var(--vp-c-gray);
}
.check {
position: absolute;
top: 1px;
left: 1px;
width: 18px;
height: 18px;
border-radius: 50%;
background-color: var(--vp-c-white);
box-shadow: var(--vp-shadow-1);
transition: background-color 0.25s, transform 0.25s;
}
.dark .check {
background-color: var(--vp-c-black);
}
.icon {
position: relative;
display: block;
width: 18px;
height: 18px;
border-radius: 50%;
overflow: hidden;
}
.icon :deep(svg) {
position: absolute;
top: 3px;
left: 3px;
width: 12px;
height: 12px;
fill: var(--vp-c-text-2);
}
.dark .icon :deep(svg) {
fill: var(--vp-c-text-1);
transition: opacity 0.25s;
}
</style>

View File

@ -0,0 +1,82 @@
<script lang="ts" setup>
import { ref, onMounted } from 'vue'
import { APPEARANCE_KEY } from '../shared/shared.ts'
import VPSwitch from './VPSwitch.vue'
import VPIconSun from './icons/VPIconSun.vue'
import VPIconMoon from './icons/VPIconMoon.vue'
const checked = ref(false)
const toggle = typeof localStorage !== 'undefined' ? useAppearance() : () => {}
onMounted(() => {
checked.value = document.documentElement.classList.contains('dark')
})
function useAppearance() {
const query = window.matchMedia('(prefers-color-scheme: dark)')
const classList = document.documentElement.classList
let userPreference = localStorage.getItem(APPEARANCE_KEY) || 'auto'
let isDark = userPreference === 'auto'
? query.matches
: userPreference === 'dark'
query.onchange = (e) => {
if (userPreference === 'auto') {
setClass((isDark = e.matches))
}
}
function toggle() {
setClass((isDark = !isDark))
userPreference = isDark
? query.matches ? 'auto' : 'dark'
: query.matches ? 'light' : 'auto'
localStorage.setItem(APPEARANCE_KEY, userPreference)
}
function setClass(dark: boolean): void {
checked.value = dark
classList[dark ? 'add' : 'remove']('dark')
}
return toggle
}
</script>
<template>
<VPSwitch
class="VPSwitchAppearance"
aria-label="toggle dark mode"
:aria-checked="checked"
@click="toggle"
>
<VPIconSun class="sun" />
<VPIconMoon class="moon" />
</VPSwitch>
</template>
<style scoped>
.sun {
opacity: 1;
}
.moon {
opacity: 0;
}
.dark .sun {
opacity: 0;
}
.dark .moon {
opacity: 1;
}
.dark .VPSwitchAppearance :deep(.check) {
transform: translateX(18px);
}
</style>

View File

@ -0,0 +1,55 @@
<script setup lang="ts">
import type { DefaultTheme } from 'vitepress/theme'
import { computed } from 'vue'
import VPTeamMembersItem from './VPTeamMembersItem.vue'
const props = defineProps<{
size?: 'small' | 'medium'
members: DefaultTheme.TeamMember[]
}>()
const classes = computed(() => [
props.size ?? 'medium',
`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="size" :member="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>

View File

@ -0,0 +1,215 @@
<script setup lang="ts">
import type { DefaultTheme } from 'vitepress/theme'
import VPIconHeart from './icons/VPIconHeart.vue'
import VPLink from './VPLink.vue'
import VPSocialLinks from './VPSocialLinks.vue'
defineProps<{
size?: 'small' | 'medium'
member: DefaultTheme.TeamMember
}>()
</script>
<template>
<article class="VPTeamMembersItem" :class="[size ?? 'medium']">
<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">
{{ member.desc }}
</p>
<div v-if="member.links" class="links">
<VPSocialLinks :links="member.links" />
</div>
</div>
</div>
<div v-if="member.sponsor" class="sp">
<VPLink class="sp-link" :href="member.sponsor" no-icon>
<VPIconHeart class="sp-icon" /> 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);
}
.desc {
margin: 0 auto;
}
.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-link:hover,
.sp-link:focus {
outline: none;
color: var(--vp-c-text-dark-1);
background-color: var(--vp-c-sponsor);
}
.sp-icon {
margin-right: 8px;
width: 16px;
height: 16px;
fill: currentColor;
}
</style>

View File

@ -0,0 +1,53 @@
<template>
<div class="VPTeamPage">
<slot />
</div>
</template>
<style scoped>
.VPTeamPage {
padding-bottom: 96px;
}
@media (min-width: 768px) {
.VPTeamPage {
padding-bottom: 128px;
}
}
: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>

View File

@ -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-light);
}
.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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,13 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
focusable="false"
height="24px"
viewBox="0 0 24 24"
width="24px"
>
<path d="M0 0h24v24H0V0z" fill="none" />
<path d="M9 5v2h6.59L4 18.59 5.41 20 17 8.41V15h2V5H9z" />
</svg>
</template>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,21 @@
import { computed } from 'vue'
import { useMediaQuery } from '@vueuse/core'
import { useSidebar } from './sidebar.js'
export function useAside() {
const { hasSidebar } = useSidebar()
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
}
}

View File

@ -0,0 +1,14 @@
import { computed } from 'vue'
import { useData } from 'vitepress'
export function useEditLink() {
const { theme, page } = useData()
return computed(() => {
const { text = 'Edit this page', pattern } = theme.value.editLink || {}
const { relativePath } = page.value
const url = pattern.replace(/:path/g, relativePath)
return { url, text }
})
}

View File

@ -0,0 +1,58 @@
import { Ref, ref, watch, readonly, onUnmounted } from 'vue'
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 (typeof window !== 'undefined') {
!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
}

View File

@ -0,0 +1,69 @@
import type { DefaultTheme } from 'vitepress/theme'
import { ref, computed, watch } from 'vue'
import { useData, useRoute } from 'vitepress'
export function useNav() {
const isScreenOpen = ref(false)
function openScreen() {
isScreenOpen.value = true
window.addEventListener('resize', closeScreenOnTabletWindow)
}
function closeScreen() {
isScreenOpen.value = false
window.removeEventListener('resize', closeScreenOnTabletWindow)
}
function toggleScreen() {
isScreenOpen.value ? closeScreen() : openScreen()
}
/**
* Close screen when the user resizes the window wider than tablet size.
*/
function closeScreenOnTabletWindow() {
window.outerWidth >= 768 && closeScreen()
}
const route = useRoute()
watch(() => route.path, closeScreen)
return {
isScreenOpen,
openScreen,
closeScreen,
toggleScreen
}
}
export function useLanguageLinks() {
const { site, localePath, theme } = useData()
return computed(() => {
const langs = site.value.langs
const localePaths = Object.keys(langs)
// one language
if (localePaths.length < 2) {
return null
}
const route = useRoute()
// intentionally remove the leading slash because each locale has one
const currentPath = route.path.replace(localePath.value, '')
const candidates = localePaths.map((localePath) => ({
text: langs[localePath].label,
link: `${localePath}${currentPath}`
}))
const selectText = theme.value.selectText || 'Languages'
return {
text: selectText,
items: candidates
} as DefaultTheme.NavItemWithChildren
})
}

View File

@ -0,0 +1,200 @@
import type { DefaultTheme } from 'vitepress/theme'
import { onMounted, onUnmounted, onUpdated, type Ref } from 'vue'
import type { Header } from '../../shared.js'
import { useAside } from '../composables/aside.js'
import { throttleAndDebounce } from '../support/utils.js'
// magic number to avoid repeated retrieval
const PAGE_OFFSET = 71
export type MenuItem = Omit<Header, 'slug' | 'children'> & {
children?: MenuItem[]
}
export function getHeaders(pageOutline: DefaultTheme.Config['outline']) {
if (pageOutline === false) return []
let updatedHeaders: MenuItem[] = []
document
.querySelectorAll<HTMLHeadingElement>('h2, h3, h4, h5, h6')
.forEach((el) => {
if (el.textContent && el.id) {
updatedHeaders.push({
level: Number(el.tagName[1]),
title: el.innerText.replace(/\s+#\s*$/, ''),
link: `#${el.id}`
})
}
})
return resolveHeaders(updatedHeaders, pageOutline)
}
export function resolveHeaders(
headers: MenuItem[],
levelsRange: Exclude<DefaultTheme.Config['outline'], false> = 2
) {
const levels: [number, number] =
typeof levelsRange === 'number'
? [levelsRange, levelsRange]
: levelsRange === 'deep'
? [2, 6]
: levelsRange
return groupHeaders(headers, levels)
}
function groupHeaders(headers: MenuItem[], levelsRange: [number, number]) {
const result: MenuItem[] = []
headers = headers.map((h) => ({ ...h }))
headers.forEach((h, index) => {
if (h.level >= levelsRange[0] && h.level <= levelsRange[1]) {
if (addToParent(index, headers, levelsRange)) {
result.push(h)
}
}
})
return result
}
function addToParent(
currIndex: number,
headers: MenuItem[],
levelsRange: [number, number]
) {
if (currIndex === 0) {
return true
}
const currentHeader = headers[currIndex]
for (let index = currIndex - 1; index >= 0; index--) {
const header = headers[index]
if (
header.level < currentHeader.level &&
header.level >= levelsRange[0] &&
header.level <= levelsRange[1]
) {
if (header.children == null) header.children = []
header.children.push(currentHeader)
return false
}
}
return true
}
export function useActiveAnchor(
container: Ref<HTMLElement>,
marker: Ref<HTMLElement>
) {
const { isAsideEnabled } = useAside()
const onScroll = throttleAndDebounce(setActiveLink, 100)
let prevActiveLink: HTMLAnchorElement | null = null
onMounted(() => {
requestAnimationFrame(setActiveLink)
window.addEventListener('scroll', onScroll)
})
onUpdated(() => {
// sidebar update means a route change
activateLink(location.hash)
})
onUnmounted(() => {
window.removeEventListener('scroll', onScroll)
})
function setActiveLink() {
if (!isAsideEnabled.value) {
return
}
const links = [].slice.call(
container.value.querySelectorAll('.outline-link')
) as HTMLAnchorElement[]
const anchors = [].slice
.call(document.querySelectorAll('.content .header-anchor'))
.filter((anchor: HTMLAnchorElement) => {
return links.some((link) => {
return link.hash === anchor.hash && anchor.offsetParent !== null
})
}) as HTMLAnchorElement[]
const scrollY = window.scrollY
const innerHeight = window.innerHeight
const offsetHeight = document.body.offsetHeight
const isBottom = Math.abs(scrollY + innerHeight - offsetHeight) < 1
// page bottom - highlight last one
if (anchors.length && isBottom) {
activateLink(anchors[anchors.length - 1].hash)
return
}
for (let i = 0; i < anchors.length; i++) {
const anchor = anchors[i]
const nextAnchor = anchors[i + 1]
const [isActive, hash] = isAnchorActive(i, anchor, nextAnchor)
if (isActive) {
activateLink(hash)
return
}
}
}
function activateLink(hash: string | null) {
if (prevActiveLink) {
prevActiveLink.classList.remove('active')
}
if (hash !== null) {
prevActiveLink = container.value.querySelector(
`a[href="${decodeURIComponent(hash)}"]`
)
}
const activeLink = prevActiveLink
if (activeLink) {
activeLink.classList.add('active')
marker.value.style.top = activeLink.offsetTop + 33 + 'px'
marker.value.style.opacity = '1'
} else {
marker.value.style.top = '33px'
marker.value.style.opacity = '0'
}
}
}
function getAnchorTop(anchor: HTMLAnchorElement): number {
return anchor.parentElement!.offsetTop - PAGE_OFFSET
}
function isAnchorActive(
index: number,
anchor: HTMLAnchorElement,
nextAnchor: HTMLAnchorElement | undefined
): [boolean, string | null] {
const scrollTop = window.scrollY
if (index === 0 && scrollTop === 0) {
return [true, null]
}
if (scrollTop < getAnchorTop(anchor)) {
return [false, null]
}
if (!nextAnchor || scrollTop < getAnchorTop(nextAnchor)) {
return [true, anchor.hash]
}
return [false, null]
}

View File

@ -0,0 +1,26 @@
import { computed } from 'vue'
import { useData } from 'vitepress'
import { isActive } from '../support/utils.js'
import { getSidebar, getFlatSideBarLinks } from '../support/sidebar.js'
export function usePrevNext() {
const { page, theme, frontmatter } = useData()
return computed(() => {
const sidebar = getSidebar(theme.value.sidebar, page.value.relativePath)
const candidates = getFlatSideBarLinks(sidebar)
const index = candidates.findIndex((link) => {
return isActive(page.value.relativePath, link.link)
})
return {
prev: frontmatter.value.prev
? { ...candidates[index - 1], text: frontmatter.value.prev }
: candidates[index - 1],
next: frontmatter.value.next
? { ...candidates[index + 1], text: frontmatter.value.next }
: candidates[index + 1]
}
})
}

View File

@ -0,0 +1,84 @@
import { computed, onMounted, onUnmounted, Ref, ref, watchEffect } from 'vue'
import { useData, useRoute } from 'vitepress'
import { getSidebar } from '../support/sidebar.js'
export function useSidebar() {
const route = useRoute()
const { theme, frontmatter } = useData()
const isOpen = ref(false)
const sidebar = computed(() => {
const sidebarConfig = theme.value.sidebar
const relativePath = route.data.relativePath
return sidebarConfig ? getSidebar(sidebarConfig, relativePath) : []
})
const hasSidebar = computed(() => {
return (
frontmatter.value.sidebar !== false &&
sidebar.value.length > 0 &&
frontmatter.value.layout !== 'home'
)
})
const hasAside = computed(() => {
return (
frontmatter.value.layout !== 'home' && frontmatter.value.aside !== false
)
})
function open() {
isOpen.value = true
}
function close() {
isOpen.value = false
}
function toggle() {
isOpen.value ? close() : open()
}
return {
isOpen,
sidebar,
hasSidebar,
hasAside,
open,
close,
toggle
}
}
/**
* a11y: cache the element that opened the Sidebar (the menu button) then
* focus that button again when Menu is closed with Escape key.
*/
export function useCloseSidebarOnEscape(
isOpen: Ref<boolean>,
close: () => void
) {
let triggerElement: HTMLButtonElement | undefined
watchEffect(() => {
triggerElement = isOpen.value
? (document.activeElement as HTMLButtonElement)
: undefined
})
onMounted(() => {
window.addEventListener('keyup', onEscape)
})
onUnmounted(() => {
window.removeEventListener('keyup', onEscape)
})
function onEscape(e: KeyboardEvent) {
if (e.key === 'Escape' && isOpen.value) {
close()
triggerElement?.focus()
}
}
}

View File

@ -0,0 +1,135 @@
import { Ref, onMounted, onUnmounted } from 'vue'
import { throttleAndDebounce } from '../support/utils.js'
export interface GridSetting {
[size: string]: [number, number][]
}
export type GridSize = 'xmini' | 'mini' | 'small' | 'medium' | 'big'
export interface UseSponsorsGridOptions {
el: Ref<HTMLElement | null>
size?: GridSize
}
/**
* Defines grid configuration for each sponsor size in tuple.
*
* [Screen width, Column size]
*
* It sets grid size on matching screen size. For example, `[768, 5]` will
* set 5 columns when screen size is bigger or equal to 768px.
*
* Column will set only when item size is bigger than the column size. For
* example, even we define 5 columns, if we only have 1 sponsor yet, we would
* like to show it in 1 column to make it stand out.
*/
const GridSettings: GridSetting = {
xmini: [[0, 2]],
mini: [],
small: [
[920, 6],
[768, 5],
[640, 4],
[480, 3],
[0, 2]
],
medium: [
[960, 5],
[832, 4],
[640, 3],
[480, 2]
],
big: [
[832, 3],
[640, 2]
]
}
export function useSponsorsGrid({
el,
size = 'medium'
}: UseSponsorsGridOptions) {
const onResize = throttleAndDebounce(manage, 100)
onMounted(() => {
manage()
window.addEventListener('resize', onResize)
})
onUnmounted(() => {
window.removeEventListener('resize', onResize)
})
function manage() {
adjustSlots(el.value!, size)
}
}
function adjustSlots(el: HTMLElement, size: GridSize) {
const tsize = el.children.length
const asize = el.querySelectorAll('.vp-sponsor-grid-item:not(.empty)').length
const grid = setGrid(el, size, asize)
manageSlots(el, grid, tsize, asize)
}
function setGrid(el: HTMLElement, size: GridSize, items: number) {
const settings = GridSettings[size]
const screen = window.innerWidth
let grid = 1
settings.some(([breakpoint, value]) => {
if (screen >= breakpoint) {
grid = items < value ? items : value
return true
}
})
setGridData(el, grid)
return grid
}
function setGridData(el: HTMLElement, value: number) {
el.dataset.vpGrid = String(value)
}
function manageSlots(
el: HTMLElement,
grid: number,
tsize: number,
asize: number
) {
const diff = tsize - asize
const rem = asize % grid
const drem = rem === 0 ? rem : grid - rem
neutralizeSlots(el, drem - diff)
}
function neutralizeSlots(el: HTMLElement, count: number) {
if (count === 0) {
return
}
count > 0 ? addSlots(el, count) : removeSlots(el, count * -1)
}
function addSlots(el: HTMLElement, count: number) {
for (let i = 0; i < count; i++) {
const slot = document.createElement('div')
slot.classList.add('vp-sponsor-grid-item', 'empty')
el.append(slot)
}
}
function removeSlots(el: HTMLElement, count: number) {
for (let i = 0; i < count; i++) {
el.removeChild(el.lastElementChild!)
}
}

Some files were not shown because too many files have changed in this diff Show More