chore: add vitepress farris theme
This commit is contained in:
parent
c9420ea44e
commit
572e0dcd97
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<div class="VPPage">
|
||||
<Content />
|
||||
</div>
|
||||
</template>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,63 @@
|
|||
<template>
|
||||
<div class="VPTeamPageTitle">
|
||||
<h1 v-if="$slots.title" class="title">
|
||||
<slot name="title" />
|
||||
</h1>
|
||||
<p v-if="$slots.lead" class="lead">
|
||||
<slot name="lead" />
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPTeamPageTitle {
|
||||
padding: 48px 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.VPTeamPageTitle {
|
||||
padding: 64px 48px 48px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.VPTeamPageTitle {
|
||||
padding: 80px 64px 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
letter-spacing: 0;
|
||||
line-height: 44px;
|
||||
font-size: 36px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.title {
|
||||
letter-spacing: -0.5px;
|
||||
line-height: 56px;
|
||||
font-size: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.lead {
|
||||
margin: 0 auto;
|
||||
max-width: 512px;
|
||||
padding-top: 12px;
|
||||
line-height: 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.lead {
|
||||
max-width: 592px;
|
||||
letter-spacing: 0.15px;
|
||||
line-height: 28px;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,8 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewBox="0 0 24 24">
|
||||
<path d="M21,11H3c-0.6,0-1-0.4-1-1s0.4-1,1-1h18c0.6,0,1,0.4,1,1S21.6,11,21,11z" />
|
||||
<path d="M21,7H3C2.4,7,2,6.6,2,6s0.4-1,1-1h18c0.6,0,1,0.4,1,1S21.6,7,21,7z" />
|
||||
<path d="M21,15H3c-0.6,0-1-0.4-1-1s0.4-1,1-1h18c0.6,0,1,0.4,1,1S21.6,15,21,15z" />
|
||||
<path d="M21,19H3c-0.6,0-1-0.4-1-1s0.4-1,1-1h18c0.6,0,1,0.4,1,1S21.6,19,21,19z" />
|
||||
</svg>
|
||||
</template>
|
|
@ -0,0 +1,8 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewBox="0 0 24 24">
|
||||
<path d="M17,11H3c-0.6,0-1-0.4-1-1s0.4-1,1-1h14c0.6,0,1,0.4,1,1S17.6,11,17,11z" />
|
||||
<path d="M21,7H3C2.4,7,2,6.6,2,6s0.4-1,1-1h18c0.6,0,1,0.4,1,1S21.6,7,21,7z" />
|
||||
<path d="M21,15H3c-0.6,0-1-0.4-1-1s0.4-1,1-1h18c0.6,0,1,0.4,1,1S21.6,15,21,15z" />
|
||||
<path d="M17,19H3c-0.6,0-1-0.4-1-1s0.4-1,1-1h14c0.6,0,1,0.4,1,1S17.6,19,17,19z" />
|
||||
</svg>
|
||||
</template>
|
|
@ -0,0 +1,8 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewBox="0 0 24 24">
|
||||
<path d="M21,11H7c-0.6,0-1-0.4-1-1s0.4-1,1-1h14c0.6,0,1,0.4,1,1S21.6,11,21,11z" />
|
||||
<path d="M21,7H3C2.4,7,2,6.6,2,6s0.4-1,1-1h18c0.6,0,1,0.4,1,1S21.6,7,21,7z" />
|
||||
<path d="M21,15H3c-0.6,0-1-0.4-1-1s0.4-1,1-1h18c0.6,0,1,0.4,1,1S21.6,15,21,15z" />
|
||||
<path d="M21,19H7c-0.6,0-1-0.4-1-1s0.4-1,1-1h14c0.6,0,1,0.4,1,1S21.6,19,21,19z" />
|
||||
</svg>
|
||||
</template>
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M19,11H7.4l5.3-5.3c0.4-0.4,0.4-1,0-1.4s-1-0.4-1.4,0l-7,7c-0.1,0.1-0.2,0.2-0.2,0.3c-0.1,0.2-0.1,0.5,0,0.8c0.1,0.1,0.1,0.2,0.2,0.3l7,7c0.2,0.2,0.5,0.3,0.7,0.3s0.5-0.1,0.7-0.3c0.4-0.4,0.4-1,0-1.4L7.4,13H19c0.6,0,1-0.4,1-1S19.6,11,19,11z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M19.9,12.4c0.1-0.2,0.1-0.5,0-0.8c-0.1-0.1-0.1-0.2-0.2-0.3l-7-7c-0.4-0.4-1-0.4-1.4,0s-0.4,1,0,1.4l5.3,5.3H5c-0.6,0-1,0.4-1,1s0.4,1,1,1h11.6l-5.3,5.3c-0.4,0.4-0.4,1,0,1.4c0.2,0.2,0.5,0.3,0.7,0.3s0.5-0.1,0.7-0.3l7-7C19.8,12.6,19.9,12.5,19.9,12.4z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewBox="0 0 24 24">
|
||||
<path d="M12,16c-0.3,0-0.5-0.1-0.7-0.3l-6-6c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l5.3,5.3l5.3-5.3c0.4-0.4,1-0.4,1.4,0s0.4,1,0,1.4l-6,6C12.5,15.9,12.3,16,12,16z" />
|
||||
</svg>
|
||||
</template>
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewBox="0 0 24 24">
|
||||
<path d="M15,19c-0.3,0-0.5-0.1-0.7-0.3l-6-6c-0.4-0.4-0.4-1,0-1.4l6-6c0.4-0.4,1-0.4,1.4,0s0.4,1,0,1.4L10.4,12l5.3,5.3c0.4,0.4,0.4,1,0,1.4C15.5,18.9,15.3,19,15,19z" />
|
||||
</svg>
|
||||
</template>
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewBox="0 0 24 24">
|
||||
<path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z" />
|
||||
</svg>
|
||||
</template>
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewBox="0 0 24 24">
|
||||
<path d="M18,16c-0.3,0-0.5-0.1-0.7-0.3L12,10.4l-5.3,5.3c-0.4,0.4-1,0.4-1.4,0s-0.4-1,0-1.4l6-6c0.4-0.4,1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4C18.5,15.9,18.3,16,18,16z" />
|
||||
</svg>
|
||||
</template>
|
|
@ -0,0 +1,6 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="M18,23H4c-1.7,0-3-1.3-3-3V6c0-1.7,1.3-3,3-3h7c0.6,0,1,0.4,1,1s-0.4,1-1,1H4C3.4,5,3,5.4,3,6v14c0,0.6,0.4,1,1,1h14c0.6,0,1-0.4,1-1v-7c0-0.6,0.4-1,1-1s1,0.4,1,1v7C21,21.7,19.7,23,18,23z" />
|
||||
<path d="M8,17c-0.3,0-0.5-0.1-0.7-0.3C7,16.5,6.9,16.1,7,15.8l1-4c0-0.2,0.1-0.3,0.3-0.5l9.5-9.5c1.2-1.2,3.2-1.2,4.4,0c1.2,1.2,1.2,3.2,0,4.4l-9.5,9.5c-0.1,0.1-0.3,0.2-0.5,0.3l-4,1C8.2,17,8.1,17,8,17zM9.9,12.5l-0.5,2.1l2.1-0.5l9.3-9.3c0.4-0.4,0.4-1.1,0-1.6c-0.4-0.4-1.2-0.4-1.6,0l0,0L9.9,12.5z M18.5,2.5L18.5,2.5L18.5,2.5z" />
|
||||
</svg>
|
||||
</template>
|
|
@ -0,0 +1,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>
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="M12,22.2c-0.3,0-0.5-0.1-0.7-0.3l-8.8-8.8c-2.5-2.5-2.5-6.7,0-9.2c2.5-2.5,6.7-2.5,9.2,0L12,4.3l0.4-0.4c0,0,0,0,0,0C13.6,2.7,15.2,2,16.9,2c0,0,0,0,0,0c1.7,0,3.4,0.7,4.6,1.9l0,0c1.2,1.2,1.9,2.9,1.9,4.6c0,1.7-0.7,3.4-1.9,4.6l-8.8,8.8C12.5,22.1,12.3,22.2,12,22.2zM7,4C5.9,4,4.7,4.4,3.9,5.3c-1.8,1.8-1.8,4.6,0,6.4l8.1,8.1l8.1-8.1c0.9-0.9,1.3-2,1.3-3.2c0-1.2-0.5-2.3-1.3-3.2l0,0C19.3,4.5,18.2,4,17,4c0,0,0,0,0,0c-1.2,0-2.3,0.5-3.2,1.3c0,0,0,0,0,0l-1.1,1.1c-0.4,0.4-1,0.4-1.4,0l-1.1-1.1C9.4,4.4,8.2,4,7,4z" />
|
||||
</svg>
|
||||
</template>
|
|
@ -0,0 +1,9 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewBox="0 0 24 24">
|
||||
<path d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path
|
||||
d=" M12.87 15.07l-2.54-2.51.03-.03c1.74-1.94 2.98-4.17 3.71-6.53H17V4h-7V2H8v2H1v1.99h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12zm-2.62 7l1.62-4.33L19.12 17h-3.24z "
|
||||
class="css-c4d79v"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="M22,13H2a1,1,0,0,1,0-2H22a1,1,0,0,1,0,2Z" />
|
||||
</svg>
|
||||
</template>
|
|
@ -0,0 +1,6 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 24 24">
|
||||
<path d="M19,2H5C3.3,2,2,3.3,2,5v14c0,1.7,1.3,3,3,3h14c1.7,0,3-1.3,3-3V5C22,3.3,20.7,2,19,2zM20,19c0,0.6-0.4,1-1,1H5c-0.6,0-1-0.4-1-1V5c0-0.6,0.4-1,1-1h14c0.6,0,1,0.4,1,1V19z" />
|
||||
<path d="M16,11H8c-0.6,0-1,0.4-1,1s0.4,1,1,1h8c0.6,0,1-0.4,1-1S16.6,11,16,11z" />
|
||||
</svg>
|
||||
</template>
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewBox="0 0 24 24">
|
||||
<path d="M12.1,22c-0.3,0-0.6,0-0.9,0c-5.5-0.5-9.5-5.4-9-10.9c0.4-4.8,4.2-8.6,9-9c0.4,0,0.8,0.2,1,0.5c0.2,0.3,0.2,0.8-0.1,1.1c-2,2.7-1.4,6.4,1.3,8.4c2.1,1.6,5,1.6,7.1,0c0.3-0.2,0.7-0.3,1.1-0.1c0.3,0.2,0.5,0.6,0.5,1c-0.2,2.7-1.5,5.1-3.6,6.8C16.6,21.2,14.4,22,12.1,22zM9.3,4.4c-2.9,1-5,3.6-5.2,6.8c-0.4,4.4,2.8,8.3,7.2,8.7c2.1,0.2,4.2-0.4,5.8-1.8c1.1-0.9,1.9-2.1,2.4-3.4c-2.5,0.9-5.3,0.5-7.5-1.1C9.2,11.4,8.1,7.7,9.3,4.4z" />
|
||||
</svg>
|
||||
</template>
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="2" />
|
||||
<circle cx="19" cy="12" r="2" />
|
||||
<circle cx="5" cy="12" r="2" />
|
||||
</svg>
|
||||
</template>
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewBox="0 0 24 24">
|
||||
<path d="M18.9,10.9h-6v-6c0-0.6-0.4-1-1-1s-1,0.4-1,1v6h-6c-0.6,0-1,0.4-1,1s0.4,1,1,1h6v6c0,0.6,0.4,1,1,1s1-0.4,1-1v-6h6c0.6,0,1-0.4,1-1S19.5,10.9,18.9,10.9z" />
|
||||
</svg>
|
||||
</template>
|
|
@ -0,0 +1,6 @@
|
|||
<template>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="M19,2H5C3.3,2,2,3.3,2,5v14c0,1.7,1.3,3,3,3h14c1.7,0,3-1.3,3-3V5C22,3.3,20.7,2,19,2z M20,19c0,0.6-0.4,1-1,1H5c-0.6,0-1-0.4-1-1V5c0-0.6,0.4-1,1-1h14c0.6,0,1,0.4,1,1V19z" />
|
||||
<path d="M16,11h-3V8c0-0.6-0.4-1-1-1s-1,0.4-1,1v3H8c-0.6,0-1,0.4-1,1s0.4,1,1,1h3v3c0,0.6,0.4,1,1,1s1-0.4,1-1v-3h3c0.6,0,1-0.4,1-1S16.6,11,16,11z" />
|
||||
</svg>
|
||||
</template>
|
|
@ -0,0 +1,13 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewBox="0 0 24 24">
|
||||
<path d="M12,18c-3.3,0-6-2.7-6-6s2.7-6,6-6s6,2.7,6,6S15.3,18,12,18zM12,8c-2.2,0-4,1.8-4,4c0,2.2,1.8,4,4,4c2.2,0,4-1.8,4-4C16,9.8,14.2,8,12,8z" />
|
||||
<path d="M12,4c-0.6,0-1-0.4-1-1V1c0-0.6,0.4-1,1-1s1,0.4,1,1v2C13,3.6,12.6,4,12,4z" />
|
||||
<path d="M12,24c-0.6,0-1-0.4-1-1v-2c0-0.6,0.4-1,1-1s1,0.4,1,1v2C13,23.6,12.6,24,12,24z" />
|
||||
<path d="M5.6,6.6c-0.3,0-0.5-0.1-0.7-0.3L3.5,4.9c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l1.4,1.4c0.4,0.4,0.4,1,0,1.4C6.2,6.5,5.9,6.6,5.6,6.6z" />
|
||||
<path d="M19.8,20.8c-0.3,0-0.5-0.1-0.7-0.3l-1.4-1.4c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l1.4,1.4c0.4,0.4,0.4,1,0,1.4C20.3,20.7,20,20.8,19.8,20.8z" />
|
||||
<path d="M3,13H1c-0.6,0-1-0.4-1-1s0.4-1,1-1h2c0.6,0,1,0.4,1,1S3.6,13,3,13z" />
|
||||
<path d="M23,13h-2c-0.6,0-1-0.4-1-1s0.4-1,1-1h2c0.6,0,1,0.4,1,1S23.6,13,23,13z" />
|
||||
<path d="M4.2,20.8c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l1.4-1.4c0.4-0.4,1-0.4,1.4,0s0.4,1,0,1.4l-1.4,1.4C4.7,20.7,4.5,20.8,4.2,20.8z" />
|
||||
<path d="M18.4,6.6c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l1.4-1.4c0.4-0.4,1-0.4,1.4,0s0.4,1,0,1.4l-1.4,1.4C18.9,6.5,18.6,6.6,18.4,6.6z" />
|
||||
</svg>
|
||||
</template>
|
|
@ -0,0 +1,21 @@
|
|||
import { 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
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
})
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
})
|
||||
}
|
|
@ -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]
|
||||
}
|
|
@ -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]
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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!)
|
||||
}
|
||||
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue