first commit

This commit is contained in:
2026-01-09 23:05:52 -05:00
commit dec0c8e4e4
4203 changed files with 824454 additions and 0 deletions

21
node_modules/vitepress/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019-present, Yuxi (Evan) You
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

28
node_modules/vitepress/README.md generated vendored Normal file
View File

@@ -0,0 +1,28 @@
# VitePress 📝💨
[![test](https://github.com/vuejs/vitepress/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/vuejs/vitepress/actions/workflows/test.yml)
[![npm](https://img.shields.io/npm/v/vitepress/next)](https://www.npmjs.com/package/vitepress/v/next)
[![nightly releases](https://img.shields.io/badge/nightly-releases-orange)](https://nightly.akryum.dev/vuejs/vitepress)
[![chat](https://img.shields.io/badge/chat-discord-blue?logo=discord)](https://chat.vuejs.org)
---
VitePress is a Vue-powered static site generator and a spiritual successor to [VuePress](https://vuepress.vuejs.org), built on top of [Vite](https://github.com/vitejs/vite).
## Documentation
To check out docs, visit [vitepress.dev](https://vitepress.dev).
## Changelog
Detailed changes for each release are documented in the [CHANGELOG](https://github.com/vuejs/vitepress/blob/main/CHANGELOG.md).
## Contribution
Please make sure to read the [Contributing Guide](https://github.com/vuejs/vitepress/blob/main/.github/contributing.md) before making a pull request.
## License
[MIT](https://github.com/vuejs/vitepress/blob/main/LICENSE)
Copyright (c) 2019-present, Yuxi (Evan) You

16
node_modules/vitepress/bin/vitepress.js generated vendored Executable file
View File

@@ -0,0 +1,16 @@
#!/usr/bin/env node
// @ts-check
import module from 'node:module'
// https://github.com/vitejs/vite/blob/6c8a5a27e645a182f5b03a4ed6aa726eab85993f/packages/vite/bin/vite.js#L48-L63
try {
module.enableCompileCache?.()
setTimeout(() => {
try {
module.flushCompileCache?.()
} catch {}
}, 10 * 1000).unref()
} catch {}
import('../dist/node/cli.js')

14
node_modules/vitepress/client.d.ts generated vendored Normal file
View File

@@ -0,0 +1,14 @@
// re-export vite client types. with strict installers like pnpm, user won't
// be able to reference vite/client in project root.
/// <reference types="vite/client" />
export * from './dist/client/index.js'
declare global {
interface WindowEventMap {
'vitepress:codeGroupTabActivate': Event & {
/** code block element that was activated */
detail: Element
}
}
}

View File

@@ -0,0 +1,10 @@
import { defineComponent, onMounted, ref } from 'vue';
export const ClientOnly = defineComponent({
setup(_, { slots }) {
const show = ref(false);
onMounted(() => {
show.value = true;
});
return () => (show.value && slots.default ? slots.default() : null);
}
});

View File

@@ -0,0 +1,24 @@
import { useData, useRoute } from 'vitepress';
import { defineComponent, h, watch } from 'vue';
import { contentUpdatedCallbacks } from '../utils';
const runCbs = () => contentUpdatedCallbacks.forEach((fn) => fn());
export const Content = defineComponent({
name: 'VitePressContent',
props: {
as: { type: [Object, String], default: 'div' }
},
setup(props) {
const route = useRoute();
const { frontmatter, site } = useData();
watch(frontmatter, runCbs, { deep: true, flush: 'post' });
return () => h(props.as, site.value.contentProps ?? { style: { position: 'relative' } }, [
route.component
? h(route.component, {
onVnodeMounted: runCbs,
onVnodeUpdated: runCbs,
onVnodeUnmounted: runCbs
})
: '404 Page Not Found'
]);
}
});

View File

@@ -0,0 +1,44 @@
import { inBrowser, onContentUpdated } from 'vitepress';
export function useCodeGroups() {
if (import.meta.env.DEV) {
onContentUpdated(() => {
document.querySelectorAll('.vp-code-group > .blocks').forEach((el) => {
Array.from(el.children).forEach((child) => {
child.classList.remove('active');
});
activate(el.children[0]);
});
});
}
if (inBrowser) {
window.addEventListener('click', (e) => {
const el = e.target;
if (el.matches('.vp-code-group input')) {
// input <- .tabs <- .vp-code-group
const group = el.parentElement?.parentElement;
if (!group)
return;
const i = Array.from(group.querySelectorAll('input')).indexOf(el);
if (i < 0)
return;
const blocks = group.querySelector('.blocks');
if (!blocks)
return;
const current = Array.from(blocks.children).find((child) => child.classList.contains('active'));
if (!current)
return;
const next = blocks.children[i];
if (!next || current === next)
return;
current.classList.remove('active');
activate(next);
const label = group?.querySelector(`label[for="${el.id}"]`);
label?.scrollIntoView({ block: 'nearest' });
}
});
}
}
function activate(el) {
el.classList.add('active');
window.dispatchEvent(new CustomEvent('vitepress:codeGroupTabActivate', { detail: el }));
}

View File

@@ -0,0 +1,77 @@
import { inBrowser } from 'vitepress';
import { isShell } from '../../shared';
const ignoredNodes = ['.vp-copy-ignore', '.diff.remove'].join(', ');
export function useCopyCode() {
if (inBrowser) {
const timeoutIdMap = new WeakMap();
window.addEventListener('click', (e) => {
const el = e.target;
if (el.matches('div[class*="language-"] > button.copy')) {
const parent = el.parentElement;
const sibling = el.nextElementSibling?.nextElementSibling; // <pre> tag
if (!parent || !sibling) {
return;
}
// Clone the node and remove the ignored nodes
const clone = sibling.cloneNode(true);
clone.querySelectorAll(ignoredNodes).forEach((node) => node.remove());
// remove extra newlines left after removing ignored nodes (affecting textContent because it is inside `<pre>`)
// doesn't affect the newlines already in the code because they are rendered as `\n<span class="line"></span>`
clone.innerHTML = clone.innerHTML.replace(/\n+/g, '\n');
let text = clone.textContent || '';
// NOTE: Any changes to this the code here may also need to update
// `transformerDisableShellSymbolSelect` in `src/node/markdown/plugins/highlight.ts`
const lang = /language-(\w+)/.exec(parent.className)?.[1] || '';
if (isShell(lang)) {
text = text.replace(/^ *(\$|>) /gm, '').trim();
}
copyToClipboard(text).then(() => {
el.classList.add('copied');
clearTimeout(timeoutIdMap.get(el));
const timeoutId = setTimeout(() => {
el.classList.remove('copied');
el.blur();
timeoutIdMap.delete(el);
}, 2000);
timeoutIdMap.set(el, timeoutId);
});
}
});
}
}
async function copyToClipboard(text) {
try {
return navigator.clipboard.writeText(text);
}
catch {
const element = document.createElement('textarea');
const previouslyFocusedElement = document.activeElement;
element.value = text;
// Prevent keyboard from showing on mobile
element.setAttribute('readonly', '');
element.style.contain = 'strict';
element.style.position = 'absolute';
element.style.left = '-9999px';
element.style.fontSize = '12pt'; // Prevent zooming on iOS
const selection = document.getSelection();
const originalRange = selection
? selection.rangeCount > 0 && selection.getRangeAt(0)
: null;
document.body.appendChild(element);
element.select();
// Explicit selection workaround for iOS
element.selectionStart = 0;
element.selectionEnd = text.length;
document.execCommand('copy');
document.body.removeChild(element);
if (originalRange) {
selection.removeAllRanges(); // originalRange can't be truthy when selection is falsy
selection.addRange(originalRange);
}
// Get the focus back on the previously focused element, if any
if (previouslyFocusedElement) {
;
previouslyFocusedElement.focus();
}
}
}

View File

@@ -0,0 +1,81 @@
import { watchEffect } from 'vue';
import { createTitle, mergeHead } from '../../shared';
export function useUpdateHead(route, siteDataByRouteRef) {
let isFirstUpdate = true;
let managedHeadElements = [];
const updateHeadTags = (newTags) => {
if (import.meta.env.PROD && isFirstUpdate) {
// in production, the initial meta tags are already pre-rendered so we
// skip the first update.
isFirstUpdate = false;
newTags.forEach((tag) => {
const headEl = createHeadElement(tag);
for (const el of document.head.children) {
if (el.isEqualNode(headEl)) {
managedHeadElements.push(el);
return;
}
}
});
return;
}
const newElements = newTags.map(createHeadElement);
managedHeadElements.forEach((oldEl, oldIndex) => {
const matchedIndex = newElements.findIndex((newEl) => newEl?.isEqualNode(oldEl ?? null));
if (matchedIndex !== -1) {
delete newElements[matchedIndex];
}
else {
oldEl?.remove();
delete managedHeadElements[oldIndex];
}
});
newElements.forEach((el) => el && document.head.appendChild(el));
managedHeadElements = [...managedHeadElements, ...newElements].filter(Boolean);
};
watchEffect(() => {
const pageData = route.data;
const siteData = siteDataByRouteRef.value;
const pageDescription = pageData && pageData.description;
const frontmatterHead = (pageData && pageData.frontmatter.head) || [];
// update title and description
const title = createTitle(siteData, pageData);
if (title !== document.title) {
document.title = title;
}
const description = pageDescription || siteData.description;
let metaDescriptionElement = document.querySelector(`meta[name=description]`);
if (metaDescriptionElement) {
if (metaDescriptionElement.getAttribute('content') !== description) {
metaDescriptionElement.setAttribute('content', description);
}
}
else {
createHeadElement(['meta', { name: 'description', content: description }]);
}
updateHeadTags(mergeHead(siteData.head, filterOutHeadDescription(frontmatterHead)));
});
}
function createHeadElement([tag, attrs, innerHTML]) {
const el = document.createElement(tag);
for (const key in attrs) {
el.setAttribute(key, attrs[key]);
}
if (innerHTML) {
el.innerHTML = innerHTML;
}
if (tag === 'script' && attrs.async == null) {
// async is true by default for dynamically created scripts
;
el.async = false;
}
return el;
}
function isMetaDescription(headConfig) {
return (headConfig[0] === 'meta' &&
headConfig[1] &&
headConfig[1].name === 'description');
}
function filterOutHeadDescription(head) {
return head.filter((h) => !isMetaDescription(h));
}

View File

@@ -0,0 +1,99 @@
// Customized pre-fetch for page chunks based on
// https://github.com/GoogleChromeLabs/quicklink
import { onMounted, onUnmounted, watch } from 'vue';
import { useRoute } from '../router';
import { inBrowser, pathToFile } from '../utils';
const hasFetched = new Set();
const createLink = () => document.createElement('link');
const viaDOM = (url) => {
const link = createLink();
link.rel = `prefetch`;
link.href = url;
document.head.appendChild(link);
};
const viaXHR = (url) => {
const req = new XMLHttpRequest();
req.open('GET', url, (req.withCredentials = true));
req.send();
};
let link;
const doFetch = inBrowser &&
(link = createLink()) &&
link.relList &&
link.relList.supports &&
link.relList.supports('prefetch')
? viaDOM
: viaXHR;
export function usePrefetch() {
if (!inBrowser) {
return;
}
if (!window.IntersectionObserver) {
return;
}
let conn;
if ((conn = navigator.connection) &&
(conn.saveData || /2g/.test(conn.effectiveType))) {
// Don't prefetch if using 2G or if Save-Data is enabled.
return;
}
const rIC = window.requestIdleCallback || setTimeout;
let observer = null;
const observeLinks = () => {
if (observer) {
observer.disconnect();
}
observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const link = entry.target;
observer.unobserve(link);
const { pathname } = link;
if (!hasFetched.has(pathname)) {
hasFetched.add(pathname);
const pageChunkPath = pathToFile(pathname);
if (pageChunkPath)
doFetch(pageChunkPath);
}
}
});
});
rIC(() => {
document
.querySelectorAll('#app a')
.forEach((link) => {
const { hostname, pathname } = new URL(link.href instanceof SVGAnimatedString
? link.href.animVal
: link.href, link.baseURI);
const extMatch = pathname.match(/\.\w+$/);
if (extMatch && extMatch[0] !== '.html') {
return;
}
if (
// only prefetch same tab navigation, since a new tab will load
// the lean js chunk instead.
link.target !== '_blank' &&
// only prefetch inbound links
hostname === location.hostname) {
if (pathname !== location.pathname) {
observer.observe(link);
}
else {
// No need to prefetch chunk for the current page, but also mark
// it as already fetched. This is because the initial page uses its
// lean chunk, and if we don't mark it, navigation to another page
// with a link back to the first page will fetch its full chunk
// which isn't needed.
hasFetched.add(pathname);
}
}
});
});
};
onMounted(observeLinks);
const route = useRoute();
watch(() => route.path, observeLinks);
onUnmounted(() => {
observer && observer.disconnect();
});
}

53
node_modules/vitepress/dist/client/app/data.js generated vendored Normal file
View File

@@ -0,0 +1,53 @@
import siteData from '@siteData';
import { useDark, usePreferredDark } from '@vueuse/core';
import { computed, inject, readonly, ref, shallowRef, watch } from 'vue';
import { APPEARANCE_KEY, createTitle, inBrowser, resolveSiteDataByRoute } from '../shared';
export const dataSymbol = Symbol();
// site data is a singleton
export const siteDataRef = shallowRef(readonly(siteData));
// per-app data
export function initData(route) {
const site = computed(() => resolveSiteDataByRoute(siteDataRef.value, route.data.relativePath));
const appearance = site.value.appearance; // fine with reactivity being lost here, config change triggers a restart
const isDark = appearance === 'force-dark'
? ref(true)
: appearance === 'force-auto'
? usePreferredDark()
: appearance
? useDark({
storageKey: APPEARANCE_KEY,
initialValue: () => (appearance === 'dark' ? 'dark' : 'auto'),
...(typeof appearance === 'object' ? appearance : {})
})
: ref(false);
const hashRef = ref(inBrowser ? location.hash : '');
if (inBrowser) {
window.addEventListener('hashchange', () => {
hashRef.value = location.hash;
});
}
watch(() => route.data, () => {
hashRef.value = inBrowser ? location.hash : '';
});
return {
site,
theme: computed(() => site.value.themeConfig),
page: computed(() => route.data),
frontmatter: computed(() => route.data.frontmatter),
params: computed(() => route.data.params),
lang: computed(() => site.value.lang),
dir: computed(() => route.data.frontmatter.dir || site.value.dir),
localeIndex: computed(() => site.value.localeIndex || 'root'),
title: computed(() => createTitle(site.value, route.data)),
description: computed(() => route.data.description || site.value.description),
isDark,
hash: computed(() => hashRef.value)
};
}
export function useData() {
const data = inject(dataSymbol);
if (!data) {
throw new Error('vitepress data not properly injected in app');
}
return data;
}

28
node_modules/vitepress/dist/client/app/devtools.js generated vendored Normal file
View File

@@ -0,0 +1,28 @@
import { setupDevToolsPlugin } from '@vue/devtools-api';
const COMPONENT_STATE_TYPE = 'VitePress';
export const setupDevtools = (app, router, data) => {
setupDevToolsPlugin({
// fix recursive reference
app: app,
id: 'org.vuejs.vitepress',
label: 'VitePress',
packageName: 'vitepress',
homepage: 'https://vitepress.dev',
componentStateTypes: [COMPONENT_STATE_TYPE]
}, (api) => {
api.on.inspectComponent((payload) => {
payload.instanceData.state.push({
type: COMPONENT_STATE_TYPE,
key: 'route',
value: router.route,
editable: false
});
payload.instanceData.state.push({
type: COMPONENT_STATE_TYPE,
key: 'data',
value: data,
editable: false
});
});
});
};

141
node_modules/vitepress/dist/client/app/index.js generated vendored Normal file
View File

@@ -0,0 +1,141 @@
import RawTheme from '@theme/index';
import { createApp as createClientApp, createSSRApp, defineComponent, h, onMounted, watchEffect } from 'vue';
import { ClientOnly } from './components/ClientOnly';
import { Content } from './components/Content';
import { useCodeGroups } from './composables/codeGroups';
import { useCopyCode } from './composables/copyCode';
import { useUpdateHead } from './composables/head';
import { usePrefetch } from './composables/preFetch';
import { dataSymbol, initData, siteDataRef, useData } from './data';
import { RouterSymbol, createRouter, scrollTo } from './router';
import { inBrowser, pathToFile } from './utils';
function resolveThemeExtends(theme) {
if (theme.extends) {
const base = resolveThemeExtends(theme.extends);
return {
...base,
...theme,
async enhanceApp(ctx) {
if (base.enhanceApp)
await base.enhanceApp(ctx);
if (theme.enhanceApp)
await theme.enhanceApp(ctx);
}
};
}
return theme;
}
const Theme = resolveThemeExtends(RawTheme);
const VitePressApp = defineComponent({
name: 'VitePressApp',
setup() {
const { site, lang, dir } = useData();
// change the language on the HTML element based on the current lang
onMounted(() => {
watchEffect(() => {
document.documentElement.lang = lang.value;
document.documentElement.dir = dir.value;
});
});
if (import.meta.env.PROD && site.value.router.prefetchLinks) {
// in prod mode, enable intersectionObserver based pre-fetch
usePrefetch();
}
// setup global copy code handler
useCopyCode();
// setup global code groups handler
useCodeGroups();
if (Theme.setup)
Theme.setup();
return () => h(Theme.Layout);
}
});
export async function createApp() {
;
globalThis.__VITEPRESS__ = true;
const router = newRouter();
const app = newApp();
app.provide(RouterSymbol, router);
const data = initData(router.route);
app.provide(dataSymbol, data);
// install global components
app.component('Content', Content);
app.component('ClientOnly', ClientOnly);
// expose $frontmatter & $params
Object.defineProperties(app.config.globalProperties, {
$frontmatter: {
get() {
return data.frontmatter.value;
}
},
$params: {
get() {
return data.page.value.params;
}
}
});
if (Theme.enhanceApp) {
await Theme.enhanceApp({
app,
router,
siteData: siteDataRef
});
}
// setup devtools in dev mode
if (import.meta.env.DEV || __VUE_PROD_DEVTOOLS__) {
import('./devtools.js').then(({ setupDevtools }) => setupDevtools(app, router, data));
}
return { app, router, data };
}
function newApp() {
return import.meta.env.PROD
? createSSRApp(VitePressApp)
: createClientApp(VitePressApp);
}
function newRouter() {
let isInitialPageLoad = inBrowser;
return createRouter((path) => {
let pageFilePath = pathToFile(path);
let pageModule = null;
if (pageFilePath) {
// use lean build if this is the initial page load
if (isInitialPageLoad) {
pageFilePath = pageFilePath.replace(/\.js$/, '.lean.js');
}
if (import.meta.env.DEV) {
pageModule = import(/*@vite-ignore*/ pageFilePath).catch(() => {
// try with/without trailing slash
// in prod this is handled in src/client/app/utils.ts#pathToFile
const url = new URL(pageFilePath, 'http://a.com');
const path = (url.pathname.endsWith('/index.md')
? url.pathname.slice(0, -9) + '.md'
: url.pathname.slice(0, -3) + '/index.md') +
url.search +
url.hash;
return import(/*@vite-ignore*/ path);
});
}
else {
pageModule = import(/*@vite-ignore*/ pageFilePath);
}
}
if (inBrowser) {
isInitialPageLoad = false;
}
return pageModule;
}, Theme.NotFound);
}
if (inBrowser) {
createApp().then(({ app, router, data }) => {
// wait until page component is fetched before mounting
router.go(location.href, { initialLoad: true }).then(() => {
// dynamically update head tags
useUpdateHead(router.route, data.site);
app.mount('#app');
// scroll to hash on new tab during dev
if (import.meta.env.DEV && location.hash) {
scrollTo(location.hash);
}
});
});
}

279
node_modules/vitepress/dist/client/app/router.js generated vendored Normal file
View File

@@ -0,0 +1,279 @@
import { inject, markRaw, nextTick, reactive, readonly } from 'vue';
import { notFoundPageData, treatAsHtml } from '../shared';
import { siteDataRef } from './data';
import { getScrollOffset, inBrowser, withBase } from './utils';
export const RouterSymbol = Symbol();
// we are just using URL to parse the pathname and hash - the base doesn't
// matter and is only passed to support same-host hrefs
const fakeHost = 'http://a.com';
const getDefaultRoute = () => ({
path: '/',
hash: '',
query: '',
component: null,
data: notFoundPageData
});
export function createRouter(loadPageModule, fallbackComponent) {
const route = reactive(getDefaultRoute());
const router = {
route,
async go(href, options) {
href = normalizeHref(href);
if ((await router.onBeforeRouteChange?.(href)) === false)
return;
if (!inBrowser || (await changeRoute(href, options)))
await loadPage(href);
syncRouteQueryAndHash();
await router.onAfterRouteChange?.(href);
}
};
let latestPendingPath = null;
async function loadPage(href, scrollPosition = 0, isRetry = false) {
if ((await router.onBeforePageLoad?.(href)) === false)
return;
const targetLoc = new URL(href, fakeHost);
const pendingPath = (latestPendingPath = targetLoc.pathname);
try {
let page = await loadPageModule(pendingPath);
if (!page)
throw new Error(`Page not found: ${pendingPath}`);
if (latestPendingPath === pendingPath) {
latestPendingPath = null;
const { default: comp, __pageData } = page;
if (!comp)
throw new Error(`Invalid route component: ${comp}`);
await router.onAfterPageLoad?.(href);
route.path = inBrowser ? pendingPath : withBase(pendingPath);
route.component = markRaw(comp);
route.data = import.meta.env.PROD
? markRaw(__pageData)
: readonly(__pageData);
syncRouteQueryAndHash(targetLoc);
if (inBrowser) {
nextTick(() => {
let actualPathname = siteDataRef.value.base +
__pageData.relativePath.replace(/(?:(^|\/)index)?\.md$/, '$1');
if (!siteDataRef.value.cleanUrls && !actualPathname.endsWith('/')) {
actualPathname += '.html';
}
if (actualPathname !== targetLoc.pathname) {
targetLoc.pathname = actualPathname;
href = actualPathname + targetLoc.search + targetLoc.hash;
history.replaceState({}, '', href);
}
return scrollTo(targetLoc.hash, false, scrollPosition);
});
}
}
}
catch (err) {
if (!/fetch|Page not found/.test(err.message) &&
!/^\/404(\.html|\/)?$/.test(href)) {
console.error(err);
}
// retry on fetch fail: the page to hash map may have been invalidated
// because a new deploy happened while the page is open. Try to fetch
// the updated pageToHash map and fetch again.
if (!isRetry) {
try {
const res = await fetch(siteDataRef.value.base + 'hashmap.json');
window.__VP_HASH_MAP__ = await res.json();
await loadPage(href, scrollPosition, true);
return;
}
catch (e) { }
}
if (latestPendingPath === pendingPath) {
latestPendingPath = null;
route.path = inBrowser ? pendingPath : withBase(pendingPath);
route.component = fallbackComponent ? markRaw(fallbackComponent) : null;
const relativePath = inBrowser
? route.path
.replace(/(^|\/)$/, '$1index')
.replace(/(\.html)?$/, '.md')
.slice(siteDataRef.value.base.length)
: '404.md';
route.data = { ...notFoundPageData, relativePath };
syncRouteQueryAndHash(targetLoc);
}
}
}
function syncRouteQueryAndHash(loc = inBrowser
? location
: { search: '', hash: '' }) {
route.query = loc.search;
route.hash = decodeURIComponent(loc.hash);
}
if (inBrowser) {
if (history.state === null)
history.replaceState({}, '');
window.addEventListener('click', (e) => {
if (e.defaultPrevented ||
!(e.target instanceof Element) ||
e.target.closest('button') || // temporary fix for docsearch action buttons
e.button !== 0 ||
e.ctrlKey ||
e.shiftKey ||
e.altKey ||
e.metaKey) {
return;
}
const link = e.target.closest('a');
if (!link ||
link.closest('.vp-raw') ||
link.hasAttribute('download') ||
link.hasAttribute('target')) {
return;
}
const linkHref = link.getAttribute('href') ??
(link instanceof SVGAElement ? link.getAttribute('xlink:href') : null);
if (linkHref == null)
return;
const { href, origin, pathname } = new URL(linkHref, link.baseURI);
const currentLoc = new URL(location.href); // copy to keep old data
// only intercept inbound html links
if (origin === currentLoc.origin && treatAsHtml(pathname)) {
e.preventDefault();
router.go(href, {
// use smooth scroll when clicking on header anchor links
smoothScroll: link.classList.contains('header-anchor')
});
}
}, { capture: true });
window.addEventListener('popstate', async (e) => {
if (e.state === null)
return;
const href = normalizeHref(location.href);
await loadPage(href, (e.state && e.state.scrollPosition) || 0);
syncRouteQueryAndHash();
await router.onAfterRouteChange?.(href);
});
window.addEventListener('hashchange', (e) => {
e.preventDefault();
syncRouteQueryAndHash();
});
}
handleHMR(route);
return router;
}
export function useRouter() {
const router = inject(RouterSymbol);
if (!router)
throw new Error('useRouter() is called without provider.');
return router;
}
export function useRoute() {
return useRouter().route;
}
export function scrollTo(hash, smooth = false, scrollPosition = 0) {
if (!hash || scrollPosition) {
window.scrollTo(0, scrollPosition);
return;
}
let target = null;
try {
target = document.getElementById(decodeURIComponent(hash).slice(1));
}
catch (e) {
console.warn(e);
}
if (!target)
return;
const targetTop = window.scrollY +
target.getBoundingClientRect().top -
getScrollOffset() +
Number.parseInt(window.getComputedStyle(target).paddingTop, 10) || 0;
const behavior = window.matchMedia('(prefers-reduced-motion)').matches
? 'instant'
: // only smooth scroll if distance is smaller than screen height
smooth && Math.abs(targetTop - window.scrollY) <= window.innerHeight
? 'smooth'
: 'auto';
const scrollToTarget = () => {
window.scrollTo({ left: 0, top: targetTop, behavior });
// focus the target element for better accessibility
target.focus({ preventScroll: true });
// return if focus worked
if (document.activeElement === target)
return;
// element has tabindex already, likely not focusable
// because of some other reason, bail out
if (target.hasAttribute('tabindex'))
return;
const restoreTabindex = () => {
target.removeAttribute('tabindex');
target.removeEventListener('blur', restoreTabindex);
};
// temporarily make the target element focusable
target.setAttribute('tabindex', '-1');
target.addEventListener('blur', restoreTabindex);
// try to focus again
target.focus({ preventScroll: true });
// remove tabindex and event listener if focus still not worked
if (document.activeElement !== target)
restoreTabindex();
};
requestAnimationFrame(scrollToTarget);
}
function handleHMR(route) {
// update route.data on HMR updates of active page
if (import.meta.hot) {
// hot reload pageData
import.meta.hot.on('vitepress:pageData', (payload) => {
if (shouldHotReload(payload))
route.data = payload.pageData;
});
}
}
function shouldHotReload(payload) {
const payloadPath = payload.path.replace(/(?:(^|\/)index)?\.md$/, '$1');
const locationPath = location.pathname
.replace(/(?:(^|\/)index)?\.html$/, '')
.slice(siteDataRef.value.base.length - 1);
return payloadPath === locationPath;
}
function normalizeHref(href) {
const url = new URL(href, fakeHost);
url.pathname = url.pathname.replace(/(^|\/)index(\.html)?$/, '$1');
// ensure correct deep link so page refresh lands on correct files
if (siteDataRef.value.cleanUrls) {
url.pathname = url.pathname.replace(/\.html$/, '');
}
else if (!url.pathname.endsWith('/') && !url.pathname.endsWith('.html')) {
url.pathname += '.html';
}
return url.pathname + url.search + url.hash;
}
async function changeRoute(href, { smoothScroll = false, initialLoad = false, replace = false } = {}) {
const loc = normalizeHref(location.href);
const nextUrl = new URL(href, location.origin);
const currentUrl = new URL(loc, location.origin);
if (href === loc) {
if (!initialLoad) {
scrollTo(nextUrl.hash, smoothScroll);
return false;
}
}
else {
if (replace) {
history.replaceState({}, '', href);
}
else {
// save scroll position before changing URL
history.replaceState({ scrollPosition: window.scrollY }, '');
history.pushState({}, '', href);
}
if (nextUrl.pathname === currentUrl.pathname) {
// scroll between hash anchors on the same page, avoid duplicate entries
if (nextUrl.hash !== currentUrl.hash) {
window.dispatchEvent(new HashChangeEvent('hashchange', {
oldURL: currentUrl.href,
newURL: nextUrl.href
}));
scrollTo(nextUrl.hash, smoothScroll);
}
return false;
}
}
return true;
}

10
node_modules/vitepress/dist/client/app/ssr.js generated vendored Normal file
View File

@@ -0,0 +1,10 @@
// entry for SSR
import { renderToString } from 'vue/server-renderer';
import { createApp } from './index';
export async function render(path) {
const { app, router } = await createApp();
await router.go(path);
const ctx = { content: '', vpSocialIcons: new Set() };
ctx.content = await renderToString(app, ctx);
return ctx;
}

1
node_modules/vitepress/dist/client/app/theme.js generated vendored Normal file
View File

@@ -0,0 +1 @@
export {};

120
node_modules/vitepress/dist/client/app/utils.js generated vendored Normal file
View File

@@ -0,0 +1,120 @@
import { tryOnUnmounted } from '@vueuse/core';
import { h, onMounted, shallowRef } from 'vue';
import { EXTERNAL_URL_RE, inBrowser, sanitizeFileName } from '../shared';
import { siteDataRef } from './data';
export { escapeHtml as _escapeHtml, inBrowser } from '../shared';
/**
* Join two paths by resolving the slash collision.
*/
export function joinPath(base, path) {
return `${base}${path}`.replace(/\/+/g, '/');
}
/**
* Append base to internal (non-relative) urls
*/
export function withBase(path) {
return EXTERNAL_URL_RE.test(path) || !path.startsWith('/')
? path
: joinPath(siteDataRef.value.base, path);
}
/**
* Converts a url path to the corresponding js chunk filename.
*/
export function pathToFile(path) {
let pagePath = path.replace(/\.html$/, '');
pagePath = decodeURIComponent(pagePath);
pagePath = pagePath.replace(/\/$/, '/index'); // /foo/ -> /foo/index
if (import.meta.env.DEV) {
// always force re-fetch content in dev
pagePath += `.md?t=${Date.now()}`;
}
else {
// in production, each .md file is built into a .md.js file following
// the path conversion scheme.
// /foo/bar.html -> ./foo_bar.md
if (inBrowser) {
const base = import.meta.env.BASE_URL;
pagePath =
sanitizeFileName(pagePath.slice(base.length).replace(/\//g, '_') || 'index') + '.md';
// client production build needs to account for page hash, which is
// injected directly in the page's html
let pageHash = __VP_HASH_MAP__[pagePath.toLowerCase()];
if (!pageHash) {
pagePath = pagePath.endsWith('_index.md')
? pagePath.slice(0, -9) + '.md'
: pagePath.slice(0, -3) + '_index.md';
pageHash = __VP_HASH_MAP__[pagePath.toLowerCase()];
}
if (!pageHash)
return null;
pagePath = `${base}${__ASSETS_DIR__}/${pagePath}.${pageHash}.js`;
}
else {
// ssr build uses much simpler name mapping
pagePath = `./${sanitizeFileName(pagePath.slice(1).replace(/\//g, '_'))}.md.js`;
}
}
return pagePath;
}
export let contentUpdatedCallbacks = [];
/**
* Register callback that is called every time the markdown content is updated
* in the DOM.
*/
export function onContentUpdated(fn) {
contentUpdatedCallbacks.push(fn);
tryOnUnmounted(() => {
contentUpdatedCallbacks = contentUpdatedCallbacks.filter((f) => f !== fn);
});
}
export function defineClientComponent(loader, args, cb) {
return {
setup() {
const comp = shallowRef();
onMounted(async () => {
let res = await loader();
// interop module default
if (res && (res.__esModule || res[Symbol.toStringTag] === 'Module')) {
res = res.default;
}
comp.value = res;
await cb?.();
});
return () => (comp.value ? h(comp.value, ...(args ?? [])) : null);
}
};
}
export function getScrollOffset() {
let scrollOffset = siteDataRef.value.scrollOffset;
let offset = 0;
let padding = 24;
if (typeof scrollOffset === 'object' && 'padding' in scrollOffset) {
padding = scrollOffset.padding;
scrollOffset = scrollOffset.selector;
}
if (typeof scrollOffset === 'number') {
offset = scrollOffset;
}
else if (typeof scrollOffset === 'string') {
offset = tryOffsetSelector(scrollOffset, padding);
}
else if (Array.isArray(scrollOffset)) {
for (const selector of scrollOffset) {
const res = tryOffsetSelector(selector, padding);
if (res) {
offset = res;
break;
}
}
}
return offset;
}
function tryOffsetSelector(selector, padding) {
const el = document.querySelector(selector);
if (!el)
return 0;
const bot = el.getBoundingClientRect().bottom;
if (bot < 0)
return 0;
return bot + padding;
}

139
node_modules/vitepress/dist/client/index.d.ts generated vendored Normal file
View File

@@ -0,0 +1,139 @@
import * as vue from 'vue';
import { Component, Ref, InjectionKey, App, AsyncComponentLoader } from 'vue';
import { PageData, Awaitable, SiteData } from '../../types/shared.js';
export { HeadConfig, Header, PageData, SiteData } from '../../types/shared.js';
declare const inBrowser: boolean;
/**
* @internal
*/
declare function escapeHtml(str: string): string;
interface Route {
path: string;
hash: string;
query: string;
data: PageData;
component: Component | null;
}
interface Router {
/**
* Current route.
*/
route: Route;
/**
* Navigate to a new URL.
*/
go: (to: string, options?: {
initialLoad?: boolean;
smoothScroll?: boolean;
replace?: boolean;
}) => Promise<void>;
/**
* Called before the route changes. Return `false` to cancel the navigation.
*/
onBeforeRouteChange?: (to: string) => Awaitable<void | boolean>;
/**
* Called before the page component is loaded (after the history state is
* updated). Return `false` to cancel the navigation.
*/
onBeforePageLoad?: (to: string) => Awaitable<void | boolean>;
/**
* Called after the page component is loaded (before the page component is updated).
*/
onAfterPageLoad?: (to: string) => Awaitable<void>;
/**
* Called after the route changes.
*/
onAfterRouteChange?: (to: string) => Awaitable<void>;
}
declare function useRouter(): Router;
declare function useRoute(): Route;
declare const dataSymbol: InjectionKey<VitePressData>;
interface VitePressData<T = any> {
/**
* Site-level metadata
*/
site: Ref<SiteData<T>>;
/**
* themeConfig from .vitepress/config.js
*/
theme: Ref<T>;
/**
* Page-level metadata
*/
page: Ref<PageData>;
/**
* page frontmatter data
*/
frontmatter: Ref<PageData['frontmatter']>;
/**
* dynamic route params
*/
params: Ref<PageData['params']>;
title: Ref<string>;
description: Ref<string>;
lang: Ref<string>;
dir: Ref<string>;
localeIndex: Ref<string>;
isDark: Ref<boolean>;
/**
* Current location hash
*/
hash: Ref<string>;
}
declare function useData<T = any>(): VitePressData<T>;
interface EnhanceAppContext {
app: App;
router: Router;
siteData: Ref<SiteData>;
}
interface Theme {
Layout?: Component;
enhanceApp?: (ctx: EnhanceAppContext) => Awaitable<void>;
extends?: Theme;
/**
* @deprecated can be replaced by wrapping layout component
*/
setup?: () => void;
/**
* @deprecated Render not found page by checking `useData().page.value.isNotFound` in Layout instead.
*/
NotFound?: Component;
}
/**
* Append base to internal (non-relative) urls
*/
declare function withBase(path: string): string;
/**
* Register callback that is called every time the markdown content is updated
* in the DOM.
*/
declare function onContentUpdated(fn: () => any): void;
declare function defineClientComponent(loader: AsyncComponentLoader, args?: any[], cb?: () => Awaitable<void>): {
setup(): () => vue.VNode<vue.RendererNode, vue.RendererElement, {
[key: string]: any;
}> | null;
};
declare function getScrollOffset(): number;
declare const Content: vue.DefineComponent<vue.ExtractPropTypes<{
as: {
type: (ObjectConstructor | StringConstructor)[];
default: string;
};
}>, () => vue.VNode<vue.RendererNode, vue.RendererElement, {
[key: string]: any;
}>, {}, {}, {}, vue.ComponentOptionsMixin, vue.ComponentOptionsMixin, {}, string, vue.PublicProps, Readonly<vue.ExtractPropTypes<{
as: {
type: (ObjectConstructor | StringConstructor)[];
default: string;
};
}>> & Readonly<{}>, {
as: string | Record<string, any>;
}, {}, {}, {}, string, vue.ComponentProvideOptions, true, {}, any>;
export { Content, type EnhanceAppContext, type Route, type Router, type Theme, type VitePressData, escapeHtml as _escapeHtml, dataSymbol, defineClientComponent, getScrollOffset, inBrowser, onContentUpdated, useData, useRoute, useRouter, withBase };

9
node_modules/vitepress/dist/client/index.js generated vendored Normal file
View File

@@ -0,0 +1,9 @@
// exports in this file are exposed to themes and md files via 'vitepress'
// so the user can do `import { useRoute, useData } from 'vitepress'`
// composables
export { dataSymbol, useData } from './app/data';
export { useRoute, useRouter } from './app/router';
// utilities
export { _escapeHtml, defineClientComponent, getScrollOffset, inBrowser, onContentUpdated, withBase } from './app/utils';
// components
export { Content } from './app/components/Content';

243
node_modules/vitepress/dist/client/shared.js generated vendored Normal file
View File

@@ -0,0 +1,243 @@
export const EXTERNAL_URL_RE = /^(?:[a-z]+:|\/\/)/i;
export const APPEARANCE_KEY = 'vitepress-theme-appearance';
export const VP_SOURCE_KEY = '[VP_SOURCE]';
const UnpackStackView = Symbol('stack-view:unpack');
const HASH_RE = /#.*$/;
const HASH_OR_QUERY_RE = /[?#].*$/;
const INDEX_OR_EXT_RE = /(?:(^|\/)index)?\.(?:md|html)$/;
export const inBrowser = typeof document !== 'undefined';
export const notFoundPageData = {
relativePath: '404.md',
filePath: '',
title: '404',
description: 'Not Found',
headers: [],
frontmatter: { sidebar: false, layout: 'page' },
lastUpdated: 0,
isNotFound: true
};
export function isActive(currentPath, matchPath, asRegex = false) {
if (matchPath === undefined) {
return false;
}
currentPath = normalize(`/${currentPath}`);
if (asRegex) {
return new RegExp(matchPath).test(currentPath);
}
if (normalize(matchPath) !== currentPath) {
return false;
}
const hashMatch = matchPath.match(HASH_RE);
if (hashMatch) {
return (inBrowser ? location.hash : '') === hashMatch[0];
}
return true;
}
export function normalize(path) {
return decodeURI(path)
.replace(HASH_OR_QUERY_RE, '')
.replace(INDEX_OR_EXT_RE, '$1');
}
export function isExternal(path) {
return EXTERNAL_URL_RE.test(path);
}
export function getLocaleForPath(siteData, relativePath) {
return (Object.keys(siteData?.locales || {}).find((key) => key !== 'root' &&
!isExternal(key) &&
isActive(relativePath, `^/${key}/`, true)) || 'root');
}
/**
* this merges the locales data to the main data by the route
*/
export function resolveSiteDataByRoute(siteData, relativePath) {
const localeIndex = getLocaleForPath(siteData, relativePath);
const { label, link, ...localeConfig } = siteData.locales[localeIndex] ?? {};
Object.assign(localeConfig, { localeIndex });
const additionalConfigs = resolveAdditionalConfig(siteData, relativePath);
if (inBrowser && import.meta.env?.DEV) {
;
localeConfig[VP_SOURCE_KEY] = `locale config (${localeIndex})`;
reportConfigLayers(relativePath, [
...additionalConfigs,
localeConfig,
siteData
]);
}
const topLayer = {
head: mergeHead(siteData.head ?? [], localeConfig.head ?? [], ...additionalConfigs.map((data) => data.head ?? []).reverse())
};
return stackView(topLayer, ...additionalConfigs, localeConfig, siteData);
}
/**
* Create the page title string based on config.
*/
export function createTitle(siteData, pageData) {
const title = pageData.title || siteData.title;
const template = pageData.titleTemplate ?? siteData.titleTemplate;
if (typeof template === 'string' && template.includes(':title')) {
return template.replace(/:title/g, title);
}
const templateString = createTitleTemplate(siteData.title, template);
if (title === templateString.slice(3)) {
return title;
}
return `${title}${templateString}`;
}
function createTitleTemplate(siteTitle, template) {
if (template === false) {
return '';
}
if (template === true || template === undefined) {
return ` | ${siteTitle}`;
}
if (siteTitle === template) {
return '';
}
return ` | ${template}`;
}
export function mergeHead(...headArrays) {
const merged = [];
const metaKeyMap = new Map();
for (const current of headArrays) {
for (const tag of current) {
const [type, attrs] = tag;
const keyAttr = Object.entries(attrs)[0];
if (type !== 'meta' || !keyAttr) {
merged.push(tag);
continue;
}
const key = `${keyAttr[0]}=${keyAttr[1]}`;
const existingIndex = metaKeyMap.get(key);
if (existingIndex != null) {
merged[existingIndex] = tag; // replace existing tag
}
else {
metaKeyMap.set(key, merged.length);
merged.push(tag);
}
}
}
return merged;
}
// https://github.com/rollup/rollup/blob/fec513270c6ac350072425cc045db367656c623b/src/utils/sanitizeFileName.ts
const INVALID_CHAR_REGEX = /[\u0000-\u001F"#$&*+,:;<=>?[\]^`{|}\u007F]/g;
const DRIVE_LETTER_REGEX = /^[a-z]:/i;
export function sanitizeFileName(name) {
const match = DRIVE_LETTER_REGEX.exec(name);
const driveLetter = match ? match[0] : '';
return (driveLetter +
name
.slice(driveLetter.length)
.replace(INVALID_CHAR_REGEX, '_')
.replace(/(^|\/)_+(?=[^/]*$)/, '$1'));
}
export function slash(p) {
return p.replace(/\\/g, '/');
}
const KNOWN_EXTENSIONS = new Set();
export function treatAsHtml(filename) {
if (KNOWN_EXTENSIONS.size === 0) {
const extraExts = (typeof process === 'object' && process.env?.VITE_EXTRA_EXTENSIONS) ||
import.meta.env?.VITE_EXTRA_EXTENSIONS ||
'';
('3g2,3gp,aac,ai,apng,au,avif,bin,bmp,cer,class,conf,crl,css,csv,dll,' +
'doc,eps,epub,exe,gif,gz,ics,ief,jar,jpe,jpeg,jpg,js,json,jsonld,m4a,' +
'man,mid,midi,mjs,mov,mp2,mp3,mp4,mpe,mpeg,mpg,mpp,oga,ogg,ogv,ogx,' +
'opus,otf,p10,p7c,p7m,p7s,pdf,png,ps,qt,roff,rtf,rtx,ser,svg,t,tif,' +
'tiff,tr,ts,tsv,ttf,txt,vtt,wav,weba,webm,webp,woff,woff2,xhtml,xml,' +
'yaml,yml,zip' +
(extraExts && typeof extraExts === 'string' ? ',' + extraExts : ''))
.split(',')
.forEach((ext) => KNOWN_EXTENSIONS.add(ext));
}
const ext = filename.split('.').pop();
return ext == null || !KNOWN_EXTENSIONS.has(ext.toLowerCase());
}
// https://github.com/sindresorhus/escape-string-regexp/blob/ba9a4473850cb367936417e97f1f2191b7cc67dd/index.js
export function escapeRegExp(str) {
return str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&').replace(/-/g, '\\x2d');
}
/**
* @internal
*/
export function escapeHtml(str) {
return str
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/&(?![\w#]+;)/g, '&amp;');
}
function resolveAdditionalConfig({ additionalConfig }, path) {
if (additionalConfig === undefined)
return [];
if (typeof additionalConfig === 'function')
return additionalConfig(path) ?? [];
const configs = [];
const segments = path.split('/').slice(0, -1); // remove file name
while (segments.length) {
const key = `/${segments.join('/')}/`;
configs.push(additionalConfig[key]);
segments.pop();
}
configs.push(additionalConfig['/']);
return configs.filter((config) => config !== undefined);
}
// This helps users to understand which configuration files are active
function reportConfigLayers(path, layers) {
const summaryTitle = `Config Layers for ${path}:`;
const summary = layers.map((c, i, arr) => {
const n = i + 1;
if (n === arr.length)
return `${n}. .vitepress/config (root)`;
return `${n}. ${c?.[VP_SOURCE_KEY] ?? '(Unknown Source)'}`;
});
console.debug([summaryTitle, ''.padEnd(summaryTitle.length, '='), ...summary].join('\n'));
}
/**
* Creates a deep, merged view of multiple objects without mutating originals.
* Returns a readonly proxy behaving like a merged object of the input objects.
* Layers are merged in descending precedence, i.e. earlier layer is on top.
*/
export function stackView(..._layers) {
const layers = _layers.filter((layer) => isObject(layer));
if (layers.length <= 1)
return _layers[0];
const allKeys = new Set(layers.flatMap((layer) => Reflect.ownKeys(layer)));
const allKeysArray = [...allKeys];
return new Proxy({}, {
// TODO: optimize for performance, this is a hot path
get(_, prop) {
if (prop === UnpackStackView)
return layers;
return stackView(...layers
.map((layer) => layer[prop])
.filter((v) => v !== undefined));
},
set() {
throw new Error('StackView is read-only and cannot be mutated.');
},
has(_, prop) {
return allKeys.has(prop);
},
ownKeys() {
return allKeysArray;
},
getOwnPropertyDescriptor(_, prop) {
for (const layer of layers) {
const descriptor = Object.getOwnPropertyDescriptor(layer, prop);
if (descriptor)
return descriptor;
}
}
});
}
stackView.unpack = function (obj) {
return obj?.[UnpackStackView];
};
export function isObject(value) {
return Object.prototype.toString.call(value) === '[object Object]';
}
const shellLangs = ['shellscript', 'shell', 'bash', 'sh', 'zsh'];
export function isShell(lang) {
return shellLangs.includes(lang);
}

View File

@@ -0,0 +1,95 @@
<script setup lang="ts">
import { computed, provide, useSlots } from 'vue'
import VPBackdrop from './components/VPBackdrop.vue'
import VPContent from './components/VPContent.vue'
import VPFooter from './components/VPFooter.vue'
import VPLocalNav from './components/VPLocalNav.vue'
import VPNav from './components/VPNav.vue'
import VPSidebar from './components/VPSidebar.vue'
import VPSkipLink from './components/VPSkipLink.vue'
import { useData } from './composables/data'
import { layoutInfoInjectionKey, registerWatchers } from './composables/layout'
import { useSidebarControl } from './composables/sidebar'
const {
isOpen: isSidebarOpen,
open: openSidebar,
close: closeSidebar
} = useSidebarControl()
registerWatchers({ closeSidebar })
const { frontmatter } = useData()
const slots = useSlots()
const heroImageSlotExists = computed(() => !!slots['home-hero-image'])
provide(layoutInfoInjectionKey, { heroImageSlotExists })
</script>
<template>
<div
v-if="frontmatter.layout !== false"
class="Layout"
:class="frontmatter.pageClass"
>
<slot name="layout-top" />
<VPSkipLink />
<VPBackdrop class="backdrop" :show="isSidebarOpen" @click="closeSidebar" />
<VPNav>
<template #nav-bar-title-before><slot name="nav-bar-title-before" /></template>
<template #nav-bar-title-after><slot name="nav-bar-title-after" /></template>
<template #nav-bar-content-before><slot name="nav-bar-content-before" /></template>
<template #nav-bar-content-after><slot name="nav-bar-content-after" /></template>
<template #nav-screen-content-before><slot name="nav-screen-content-before" /></template>
<template #nav-screen-content-after><slot name="nav-screen-content-after" /></template>
</VPNav>
<VPLocalNav :open="isSidebarOpen" @open-menu="openSidebar" />
<VPSidebar :open="isSidebarOpen">
<template #sidebar-nav-before><slot name="sidebar-nav-before" /></template>
<template #sidebar-nav-after><slot name="sidebar-nav-after" /></template>
</VPSidebar>
<VPContent>
<template #page-top><slot name="page-top" /></template>
<template #page-bottom><slot name="page-bottom" /></template>
<template #not-found><slot name="not-found" /></template>
<template #home-hero-before><slot name="home-hero-before" /></template>
<template #home-hero-info-before><slot name="home-hero-info-before" /></template>
<template #home-hero-info><slot name="home-hero-info" /></template>
<template #home-hero-info-after><slot name="home-hero-info-after" /></template>
<template #home-hero-actions-after><slot name="home-hero-actions-after" /></template>
<template #home-hero-image><slot name="home-hero-image" /></template>
<template #home-hero-after><slot name="home-hero-after" /></template>
<template #home-features-before><slot name="home-features-before" /></template>
<template #home-features-after><slot name="home-features-after" /></template>
<template #doc-footer-before><slot name="doc-footer-before" /></template>
<template #doc-before><slot name="doc-before" /></template>
<template #doc-after><slot name="doc-after" /></template>
<template #doc-top><slot name="doc-top" /></template>
<template #doc-bottom><slot name="doc-bottom" /></template>
<template #aside-top><slot name="aside-top" /></template>
<template #aside-bottom><slot name="aside-bottom" /></template>
<template #aside-outline-before><slot name="aside-outline-before" /></template>
<template #aside-outline-after><slot name="aside-outline-after" /></template>
<template #aside-ads-before><slot name="aside-ads-before" /></template>
<template #aside-ads-after><slot name="aside-ads-after" /></template>
</VPContent>
<VPFooter />
<slot name="layout-bottom" />
</div>
<Content v-else />
</template>
<style scoped>
.Layout {
display: flex;
flex-direction: column;
min-height: 100vh;
}
</style>

View File

@@ -0,0 +1,96 @@
<script setup lang="ts">
import { withBase } from 'vitepress'
import { useData } from './composables/data'
import { useLangs } from './composables/langs'
const { theme } = useData()
const { currentLang } = useLangs()
</script>
<template>
<div class="NotFound">
<p class="code">{{ theme.notFound?.code ?? '404' }}</p>
<h1 class="title">{{ theme.notFound?.title ?? 'PAGE NOT FOUND' }}</h1>
<div class="divider" />
<blockquote class="quote">
{{
theme.notFound?.quote ??
"But if you don't change your direction, and if you keep looking, you may end up where you are heading."
}}
</blockquote>
<div class="action">
<a
class="link"
:href="withBase(theme.notFound?.link ?? currentLang.link)"
:aria-label="theme.notFound?.linkLabel ?? 'go to home'"
>
{{ theme.notFound?.linkText ?? 'Take me home' }}
</a>
</div>
</div>
</template>
<style scoped>
.NotFound {
padding: 64px 24px 96px;
text-align: center;
}
@media (min-width: 768px) {
.NotFound {
padding: 96px 32px 168px;
}
}
.code {
line-height: 64px;
font-size: 64px;
font-weight: 600;
}
.title {
padding-top: 12px;
letter-spacing: 2px;
line-height: 20px;
font-size: 20px;
font-weight: 700;
}
.divider {
margin: 24px auto 18px;
width: 64px;
height: 1px;
background-color: var(--vp-c-divider);
}
.quote {
margin: 0 auto;
max-width: 256px;
font-size: 14px;
font-weight: 500;
color: var(--vp-c-text-2);
}
.action {
padding-top: 20px;
}
.link {
display: inline-block;
border: 1px solid var(--vp-c-brand-1);
border-radius: 16px;
padding: 3px 16px;
font-size: 14px;
font-weight: 500;
color: var(--vp-c-brand-1);
transition:
border-color 0.25s,
color 0.25s;
}
.link:hover {
border-color: var(--vp-c-brand-2);
color: var(--vp-c-brand-2);
}
</style>

View File

@@ -0,0 +1,92 @@
<script setup lang="ts">
import docsearch from '@docsearch/js'
import { useRouter } from 'vitepress'
import type { DefaultTheme } from 'vitepress/theme'
import { nextTick, onMounted, watch } from 'vue'
import { useData } from '../composables/data'
const props = defineProps<{
algolia: DefaultTheme.AlgoliaSearchOptions
}>()
const router = useRouter()
const { site, localeIndex, lang } = useData()
onMounted(update)
watch(localeIndex, update)
async function update() {
await nextTick()
const options = {
...props.algolia,
...props.algolia.locales?.[localeIndex.value]
}
const rawFacetFilters = options.searchParameters?.facetFilters ?? []
const facetFilters = [
...(Array.isArray(rawFacetFilters)
? rawFacetFilters
: [rawFacetFilters]
).filter((f) => !f.startsWith('lang:')),
`lang:${lang.value}`
]
// Rebuild the askAi prop as an object:
// If the askAi prop is a string, treat it as the assistantId and use
// the default indexName, apiKey and appId from the main options.
// If the askAi prop is an object, spread its explicit values.
const askAiProp = options.askAi
const isAskAiString = typeof askAiProp === 'string'
const askAi = askAiProp
? {
indexName: isAskAiString ? options.indexName : askAiProp.indexName,
apiKey: isAskAiString ? options.apiKey : askAiProp.apiKey,
appId: isAskAiString ? options.appId : askAiProp.appId,
assistantId: isAskAiString ? askAiProp : askAiProp.assistantId,
// Re-use the merged facetFilters from the search parameters so that
// Ask AI uses the same language filtering as the regular search.
searchParameters: facetFilters.length ? { facetFilters } : undefined
}
: undefined
initialize({
...options,
searchParameters: {
...options.searchParameters,
facetFilters
},
askAi
})
}
function initialize(userOptions: DefaultTheme.AlgoliaSearchOptions) {
const options = Object.assign({}, userOptions, {
container: '#docsearch',
navigator: {
navigate(item: { itemUrl: string }) {
router.go(item.itemUrl)
}
},
transformItems(items: { url: string }[]) {
return items.map((item) => {
return Object.assign({}, item, {
url: getRelativePath(item.url)
})
})
}
})
docsearch(options as any)
}
function getRelativePath(url: string) {
const { pathname, hash } = new URL(url, location.origin)
return pathname.replace(/\.html$/, site.value.cleanUrls ? '' : '.html') + hash
}
</script>
<template>
<div id="docsearch" />
</template>

View File

@@ -0,0 +1,41 @@
<script lang="ts" setup>
defineProps<{
show: boolean
}>()
</script>
<template>
<transition name="fade">
<div v-if="show" class="VPBackdrop" />
</transition>
</template>
<style scoped>
.VPBackdrop {
position: fixed;
top: 0;
/*rtl:ignore*/
right: 0;
bottom: 0;
/*rtl:ignore*/
left: 0;
z-index: var(--vp-z-index-backdrop);
background: var(--vp-backdrop-bg-color);
transition: opacity 0.5s;
}
.VPBackdrop.fade-enter-from,
.VPBackdrop.fade-leave-to {
opacity: 0;
}
.VPBackdrop.fade-leave-active {
transition-duration: .25s;
}
@media (min-width: 1280px) {
.VPBackdrop {
display: none;
}
}
</style>

View File

@@ -0,0 +1,87 @@
<script setup lang="ts">
interface Props {
text?: string
type?: 'info' | 'tip' | 'warning' | 'danger'
}
withDefaults(defineProps<Props>(), {
type: 'tip'
})
</script>
<template>
<span class="VPBadge" :class="type">
<slot>{{ text }}</slot>
</span>
</template>
<style>
.VPBadge {
display: inline-block;
margin-left: 2px;
border: 1px solid transparent;
border-radius: 12px;
padding: 0 10px;
line-height: 22px;
font-size: 12px;
font-weight: 500;
white-space: nowrap;
transform: translateY(-2px);
}
.VPBadge.small {
padding: 0 6px;
line-height: 18px;
font-size: 10px;
transform: translateY(-8px);
}
.VPDocFooter .VPBadge {
display: none;
}
.vp-doc h1 > .VPBadge {
margin-top: 4px;
vertical-align: top;
}
.vp-doc h2 > .VPBadge {
margin-top: 3px;
padding: 0 8px;
vertical-align: top;
}
.vp-doc h3 > .VPBadge {
vertical-align: middle;
}
.vp-doc h4 > .VPBadge,
.vp-doc h5 > .VPBadge,
.vp-doc h6 > .VPBadge {
vertical-align: middle;
line-height: 18px;
}
.VPBadge.info {
border-color: var(--vp-badge-info-border);
color: var(--vp-badge-info-text);
background-color: var(--vp-badge-info-bg);
}
.VPBadge.tip {
border-color: var(--vp-badge-tip-border);
color: var(--vp-badge-tip-text);
background-color: var(--vp-badge-tip-bg);
}
.VPBadge.warning {
border-color: var(--vp-badge-warning-border);
color: var(--vp-badge-warning-text);
background-color: var(--vp-badge-warning-bg);
}
.VPBadge.danger {
border-color: var(--vp-badge-danger-border);
color: var(--vp-badge-danger-text);
background-color: var(--vp-badge-danger-bg);
}
</style>

View File

@@ -0,0 +1,123 @@
<script setup lang="ts">
import { computed } from 'vue'
import { normalizeLink } from '../support/utils'
import { EXTERNAL_URL_RE } from '../../shared'
interface Props {
tag?: string
size?: 'medium' | 'big'
theme?: 'brand' | 'alt' | 'sponsor'
text?: string
href?: string
target?: string;
rel?: string;
}
const props = withDefaults(defineProps<Props>(), {
size: 'medium',
theme: 'brand'
})
const isExternal = computed(
() => props.href && EXTERNAL_URL_RE.test(props.href)
)
const component = computed(() => {
return props.tag || (props.href ? 'a' : 'button')
})
</script>
<template>
<component
:is="component"
class="VPButton"
:class="[size, theme]"
:href="href ? normalizeLink(href) : undefined"
:target="props.target ?? (isExternal ? '_blank' : undefined)"
:rel="props.rel ?? (isExternal ? 'noreferrer' : undefined)"
>
<slot>{{ text }}</slot>
</component>
</template>
<style scoped>
.VPButton {
display: inline-block;
border: 1px solid transparent;
text-align: center;
font-weight: 600;
white-space: nowrap;
transition: color 0.25s, border-color 0.25s, background-color 0.25s;
}
.VPButton:active {
transition: color 0.1s, border-color 0.1s, background-color 0.1s;
}
.VPButton.medium {
border-radius: 20px;
padding: 0 20px;
line-height: 38px;
font-size: 14px;
}
.VPButton.big {
border-radius: 24px;
padding: 0 24px;
line-height: 46px;
font-size: 16px;
}
.VPButton.brand {
border-color: var(--vp-button-brand-border);
color: var(--vp-button-brand-text);
background-color: var(--vp-button-brand-bg);
}
.VPButton.brand:hover {
border-color: var(--vp-button-brand-hover-border);
color: var(--vp-button-brand-hover-text);
background-color: var(--vp-button-brand-hover-bg);
}
.VPButton.brand:active {
border-color: var(--vp-button-brand-active-border);
color: var(--vp-button-brand-active-text);
background-color: var(--vp-button-brand-active-bg);
}
.VPButton.alt {
border-color: var(--vp-button-alt-border);
color: var(--vp-button-alt-text);
background-color: var(--vp-button-alt-bg);
}
.VPButton.alt:hover {
border-color: var(--vp-button-alt-hover-border);
color: var(--vp-button-alt-hover-text);
background-color: var(--vp-button-alt-hover-bg);
}
.VPButton.alt:active {
border-color: var(--vp-button-alt-active-border);
color: var(--vp-button-alt-active-text);
background-color: var(--vp-button-alt-active-bg);
}
.VPButton.sponsor {
border-color: var(--vp-button-sponsor-border);
color: var(--vp-button-sponsor-text);
background-color: var(--vp-button-sponsor-bg);
}
.VPButton.sponsor:hover {
border-color: var(--vp-button-sponsor-hover-border);
color: var(--vp-button-sponsor-hover-text);
background-color: var(--vp-button-sponsor-hover-bg);
}
.VPButton.sponsor:active {
border-color: var(--vp-button-sponsor-active-border);
color: var(--vp-button-sponsor-active-text);
background-color: var(--vp-button-sponsor-active-bg);
}
</style>

View File

@@ -0,0 +1,109 @@
<script setup lang="ts">
import type { DefaultTheme } from 'vitepress/theme'
import { ref, watch, onMounted } from 'vue'
import { useAside } from '../composables/aside'
import { useData } from '../composables/data'
const { page } = useData()
const props = defineProps<{
carbonAds: DefaultTheme.CarbonAdsOptions
}>()
const carbonOptions = props.carbonAds
const { isAsideEnabled } = useAside()
const container = ref()
let isInitialized = false
function init() {
if (!isInitialized) {
isInitialized = true
const s = document.createElement('script')
s.id = '_carbonads_js'
s.src = `//cdn.carbonads.com/carbon.js?serve=${carbonOptions.code}&placement=${carbonOptions.placement}`
s.async = true
container.value.appendChild(s)
}
}
watch(() => page.value.relativePath, () => {
if (isInitialized && isAsideEnabled.value) {
;(window as any)._carbonads?.refresh()
}
})
// no need to account for option changes during dev, we can just
// refresh the page
if (carbonOptions) {
onMounted(() => {
// if the page is loaded when aside is active, load carbon directly.
// otherwise, only load it if the page resizes to wide enough. this avoids
// loading carbon at all on mobile where it's never shown
if (isAsideEnabled.value) {
init()
} else {
watch(isAsideEnabled, (wide) => wide && init())
}
})
}
</script>
<template>
<div class="VPCarbonAds" ref="container" />
</template>
<style scoped>
.VPCarbonAds {
display: flex;
justify-content: center;
align-items: center;
padding: 24px;
border-radius: 12px;
min-height: 256px;
text-align: center;
line-height: 18px;
font-size: 12px;
font-weight: 500;
background-color: var(--vp-carbon-ads-bg-color);
}
.VPCarbonAds :deep(img) {
margin: 0 auto;
border-radius: 6px;
}
.VPCarbonAds :deep(.carbon-text) {
display: block;
margin: 0 auto;
padding-top: 12px;
color: var(--vp-carbon-ads-text-color);
transition: color 0.25s;
}
.VPCarbonAds :deep(.carbon-text:hover) {
color: var(--vp-carbon-ads-hover-text-color);
}
.VPCarbonAds :deep(.carbon-poweredby) {
display: block;
padding-top: 6px;
font-size: 11px;
font-weight: 500;
color: var(--vp-carbon-ads-poweredby-color);
text-transform: uppercase;
transition: color 0.25s;
}
.VPCarbonAds :deep(.carbon-poweredby:hover) {
color: var(--vp-carbon-ads-hover-poweredby-color);
}
.VPCarbonAds :deep(> div) {
display: none;
}
.VPCarbonAds :deep(> div:first-of-type) {
display: block;
}
</style>

View File

@@ -0,0 +1,95 @@
<script setup lang="ts">
import NotFound from '../NotFound.vue'
import { useData } from '../composables/data'
import { useLayout } from '../composables/layout'
import VPDoc from './VPDoc.vue'
import VPHome from './VPHome.vue'
import VPPage from './VPPage.vue'
const { page, frontmatter } = useData()
const { isHome, hasSidebar } = useLayout()
</script>
<template>
<div
class="VPContent"
id="VPContent"
:class="{ 'has-sidebar': hasSidebar, 'is-home': isHome }"
>
<slot name="not-found" v-if="page.isNotFound"><NotFound /></slot>
<VPPage v-else-if="frontmatter.layout === 'page'">
<template #page-top><slot name="page-top" /></template>
<template #page-bottom><slot name="page-bottom" /></template>
</VPPage>
<VPHome v-else-if="frontmatter.layout === 'home'">
<template #home-hero-before><slot name="home-hero-before" /></template>
<template #home-hero-info-before><slot name="home-hero-info-before" /></template>
<template #home-hero-info><slot name="home-hero-info" /></template>
<template #home-hero-info-after><slot name="home-hero-info-after" /></template>
<template #home-hero-actions-after><slot name="home-hero-actions-after" /></template>
<template #home-hero-image><slot name="home-hero-image" /></template>
<template #home-hero-after><slot name="home-hero-after" /></template>
<template #home-features-before><slot name="home-features-before" /></template>
<template #home-features-after><slot name="home-features-after" /></template>
</VPHome>
<component
v-else-if="frontmatter.layout && frontmatter.layout !== 'doc'"
:is="frontmatter.layout"
/>
<VPDoc v-else>
<template #doc-top><slot name="doc-top" /></template>
<template #doc-bottom><slot name="doc-bottom" /></template>
<template #doc-footer-before><slot name="doc-footer-before" /></template>
<template #doc-before><slot name="doc-before" /></template>
<template #doc-after><slot name="doc-after" /></template>
<template #aside-top><slot name="aside-top" /></template>
<template #aside-outline-before><slot name="aside-outline-before" /></template>
<template #aside-outline-after><slot name="aside-outline-after" /></template>
<template #aside-ads-before><slot name="aside-ads-before" /></template>
<template #aside-ads-after><slot name="aside-ads-after" /></template>
<template #aside-bottom><slot name="aside-bottom" /></template>
</VPDoc>
</div>
</template>
<style scoped>
.VPContent {
flex-grow: 1;
flex-shrink: 0;
margin: var(--vp-layout-top-height, 0px) auto 0;
width: 100%;
}
.VPContent.is-home {
width: 100%;
max-width: 100%;
}
.VPContent.has-sidebar {
margin: 0;
}
@media (min-width: 960px) {
.VPContent {
padding-top: var(--vp-nav-height);
}
.VPContent.has-sidebar {
margin: var(--vp-layout-top-height, 0px) 0 0;
padding-left: var(--vp-sidebar-width);
}
}
@media (min-width: 1440px) {
.VPContent.has-sidebar {
padding-right: calc((100% - var(--vp-layout-max-width)) / 2);
padding-left: calc((100% - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width));
}
}
</style>

View File

@@ -0,0 +1,194 @@
<script setup lang="ts">
import { useRoute } from 'vitepress'
import { computed } from 'vue'
import { useData } from '../composables/data'
import { useLayout } from '../composables/layout'
import VPDocAside from './VPDocAside.vue'
import VPDocFooter from './VPDocFooter.vue'
const { theme } = useData()
const route = useRoute()
const { hasSidebar, hasAside, leftAside } = useLayout()
const pageName = computed(() =>
route.path.replace(/[./]+/g, '_').replace(/_html$/, '')
)
</script>
<template>
<div
class="VPDoc"
:class="{ 'has-sidebar': hasSidebar, 'has-aside': hasAside }"
>
<slot name="doc-top" />
<div class="container">
<div v-if="hasAside" class="aside" :class="{'left-aside': leftAside}">
<div class="aside-curtain" />
<div class="aside-container">
<div class="aside-content">
<VPDocAside>
<template #aside-top><slot name="aside-top" /></template>
<template #aside-bottom><slot name="aside-bottom" /></template>
<template #aside-outline-before><slot name="aside-outline-before" /></template>
<template #aside-outline-after><slot name="aside-outline-after" /></template>
<template #aside-ads-before><slot name="aside-ads-before" /></template>
<template #aside-ads-after><slot name="aside-ads-after" /></template>
</VPDocAside>
</div>
</div>
</div>
<div class="content">
<div class="content-container">
<slot name="doc-before" />
<main class="main">
<Content
class="vp-doc"
:class="[
pageName,
theme.externalLinkIcon && 'external-link-icon-enabled'
]"
/>
</main>
<VPDocFooter>
<template #doc-footer-before><slot name="doc-footer-before" /></template>
</VPDocFooter>
<slot name="doc-after" />
</div>
</div>
</div>
<slot name="doc-bottom" />
</div>
</template>
<style scoped>
.VPDoc {
padding: 32px 24px 96px;
width: 100%;
}
@media (min-width: 768px) {
.VPDoc {
padding: 48px 32px 128px;
}
}
@media (min-width: 960px) {
.VPDoc {
padding: 48px 32px 0;
}
.VPDoc:not(.has-sidebar) .container {
display: flex;
justify-content: center;
max-width: 992px;
}
.VPDoc:not(.has-sidebar) .content {
max-width: 752px;
}
}
@media (min-width: 1280px) {
.VPDoc .container {
display: flex;
justify-content: center;
}
.VPDoc .aside {
display: block;
}
}
@media (min-width: 1440px) {
.VPDoc:not(.has-sidebar) .content {
max-width: 784px;
}
.VPDoc:not(.has-sidebar) .container {
max-width: 1104px;
}
}
.container {
margin: 0 auto;
width: 100%;
}
.aside {
position: relative;
display: none;
order: 2;
flex-grow: 1;
padding-left: 32px;
width: 100%;
max-width: 256px;
}
.left-aside {
order: 1;
padding-left: unset;
padding-right: 32px;
}
.aside-container {
position: fixed;
top: 0;
padding-top: calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + var(--vp-doc-top-height, 0px) + 48px);
width: 224px;
height: 100vh;
overflow-x: hidden;
overflow-y: auto;
scrollbar-width: none;
}
.aside-container::-webkit-scrollbar {
display: none;
}
.aside-curtain {
position: fixed;
bottom: 0;
z-index: 10;
width: 224px;
height: 32px;
background: linear-gradient(transparent, var(--vp-c-bg) 70%);
pointer-events: none;
}
.aside-content {
display: flex;
flex-direction: column;
min-height: calc(100vh - (var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 48px));
padding-bottom: 32px;
}
.content {
position: relative;
margin: 0 auto;
width: 100%;
}
@media (min-width: 960px) {
.content {
padding: 0 32px 128px;
}
}
@media (min-width: 1280px) {
.content {
order: 1;
margin: 0;
min-width: 640px;
}
}
.content-container {
margin: 0 auto;
}
.VPDoc.has-aside .content-container {
max-width: 688px;
}
</style>

View File

@@ -0,0 +1,46 @@
<script setup lang="ts">
import { useData } from '../composables/data'
import VPDocAsideOutline from './VPDocAsideOutline.vue'
import VPDocAsideCarbonAds from './VPDocAsideCarbonAds.vue'
const { theme } = useData()
</script>
<template>
<div class="VPDocAside">
<slot name="aside-top" />
<slot name="aside-outline-before" />
<VPDocAsideOutline />
<slot name="aside-outline-after" />
<div class="spacer" />
<slot name="aside-ads-before" />
<VPDocAsideCarbonAds v-if="theme.carbonAds" :carbon-ads="theme.carbonAds" />
<slot name="aside-ads-after" />
<slot name="aside-bottom" />
</div>
</template>
<style scoped>
.VPDocAside {
display: flex;
flex-direction: column;
flex-grow: 1;
}
.spacer {
flex-grow: 1;
}
.VPDocAside :deep(.spacer + .VPDocAsideSponsors),
.VPDocAside :deep(.spacer + .VPDocAsideCarbonAds) {
margin-top: 24px;
}
.VPDocAside :deep(.VPDocAsideSponsors + .VPDocAsideCarbonAds) {
margin-top: 16px;
}
</style>

View File

@@ -0,0 +1,18 @@
<script setup lang="ts">
import { defineAsyncComponent } from 'vue'
import type { DefaultTheme } from 'vitepress/theme'
defineProps<{
carbonAds: DefaultTheme.CarbonAdsOptions
}>()
const VPCarbonAds = __CARBON__
? defineAsyncComponent(() => import('./VPCarbonAds.vue'))
: () => null
</script>
<template>
<div class="VPDocAsideCarbonAds">
<VPCarbonAds :carbon-ads="carbonAds" />
</div>
</template>

View File

@@ -0,0 +1,80 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useData } from '../composables/data'
import { resolveTitle, useActiveAnchor } from '../composables/outline'
import VPDocOutlineItem from './VPDocOutlineItem.vue'
import { useLayout } from '../composables/layout'
const { theme } = useData()
const container = ref()
const marker = ref()
const { headers, hasLocalNav } = useLayout()
useActiveAnchor(container, marker)
</script>
<template>
<nav
aria-labelledby="doc-outline-aria-label"
class="VPDocAsideOutline"
:class="{ 'has-outline': hasLocalNav }"
ref="container"
>
<div class="content">
<div class="outline-marker" ref="marker" />
<div
aria-level="2"
class="outline-title"
id="doc-outline-aria-label"
role="heading"
>
{{ resolveTitle(theme) }}
</div>
<VPDocOutlineItem :headers :root="true" />
</div>
</nav>
</template>
<style scoped>
.VPDocAsideOutline {
display: none;
}
.VPDocAsideOutline.has-outline {
display: block;
}
.content {
position: relative;
border-left: 1px solid var(--vp-c-divider);
padding-left: 16px;
font-size: 13px;
font-weight: 500;
}
.outline-marker {
position: absolute;
top: 32px;
left: -1px;
z-index: 0;
opacity: 0;
width: 2px;
border-radius: 2px;
height: 18px;
background-color: var(--vp-c-brand-1);
transition:
top 0.25s cubic-bezier(0, 1, 0.5, 1),
background-color 0.5s,
opacity 0.25s;
}
.outline-title {
line-height: 32px;
font-size: 14px;
font-weight: 600;
}
</style>

View File

@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { Sponsors } from './VPSponsors.vue'
import type { Sponsor } from './VPSponsorsGrid.vue'
import VPSponsors from './VPSponsors.vue'
defineProps<{
tier?: string
size?: 'xmini' | 'mini' | 'small'
data: Sponsors[] | Sponsor[]
}>()
</script>
<template>
<div class="VPDocAsideSponsors">
<VPSponsors mode="aside" :tier :size :data />
</div>
</template>

View File

@@ -0,0 +1,167 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useData } from '../composables/data'
import { useEditLink } from '../composables/edit-link'
import { usePrevNext } from '../composables/prev-next'
import VPLink from './VPLink.vue'
import VPDocFooterLastUpdated from './VPDocFooterLastUpdated.vue'
const { theme, page, frontmatter } = useData()
const editLink = useEditLink()
const control = usePrevNext()
const hasEditLink = computed(
() => theme.value.editLink && frontmatter.value.editLink !== false
)
const hasLastUpdated = computed(() => page.value.lastUpdated)
const showFooter = computed(
() =>
hasEditLink.value ||
hasLastUpdated.value ||
control.value.prev ||
control.value.next
)
</script>
<template>
<footer v-if="showFooter" class="VPDocFooter">
<slot name="doc-footer-before" />
<div v-if="hasEditLink || hasLastUpdated" class="edit-info">
<div v-if="hasEditLink" class="edit-link">
<VPLink class="edit-link-button" :href="editLink.url" :no-icon="true">
<span class="vpi-square-pen edit-link-icon" />
{{ editLink.text }}
</VPLink>
</div>
<div v-if="hasLastUpdated" class="last-updated">
<VPDocFooterLastUpdated />
</div>
</div>
<nav
v-if="control.prev?.link || control.next?.link"
class="prev-next"
aria-labelledby="doc-footer-aria-label"
>
<span class="visually-hidden" id="doc-footer-aria-label">Pager</span>
<div class="pager">
<VPLink
v-if="control.prev?.link"
class="pager-link prev"
:href="control.prev.link"
>
<span
class="desc"
v-html="theme.docFooter?.prev || 'Previous page'"
></span>
<span class="title" v-html="control.prev.text"></span>
</VPLink>
</div>
<div class="pager">
<VPLink
v-if="control.next?.link"
class="pager-link next"
:href="control.next.link"
>
<span
class="desc"
v-html="theme.docFooter?.next || 'Next page'"
></span>
<span class="title" v-html="control.next.text"></span>
</VPLink>
</div>
</nav>
</footer>
</template>
<style scoped>
.VPDocFooter {
margin-top: 64px;
}
.edit-info {
padding-bottom: 18px;
}
@media (min-width: 640px) {
.edit-info {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 14px;
}
}
.edit-link-button {
display: flex;
align-items: center;
border: 0;
line-height: 32px;
font-size: 14px;
font-weight: 500;
color: var(--vp-c-brand-1);
transition: color 0.25s;
}
.edit-link-button:hover {
color: var(--vp-c-brand-2);
}
.edit-link-icon {
margin-right: 8px;
}
.prev-next {
border-top: 1px solid var(--vp-c-divider);
padding-top: 24px;
display: grid;
grid-row-gap: 8px;
}
@media (min-width: 640px) {
.prev-next {
grid-template-columns: repeat(2, 1fr);
grid-column-gap: 16px;
}
}
.pager-link {
display: block;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 11px 16px 13px;
width: 100%;
height: 100%;
transition: border-color 0.25s;
}
.pager-link:hover {
border-color: var(--vp-c-brand-1);
}
.pager-link.next {
margin-left: auto;
text-align: right;
}
.desc {
display: block;
line-height: 20px;
font-size: 12px;
font-weight: 500;
color: var(--vp-c-text-2);
}
.title {
display: block;
line-height: 20px;
font-size: 14px;
font-weight: 500;
color: var(--vp-c-brand-1);
transition: color 0.25s;
}
</style>

View File

@@ -0,0 +1,62 @@
<script setup lang="ts">
import { useNavigatorLanguage } from '@vueuse/core'
import { computed, onMounted, shallowRef, useTemplateRef, watchEffect } from 'vue'
import { useData } from '../composables/data'
const { theme, page, lang: pageLang } = useData()
const { language: browserLang } = useNavigatorLanguage()
const timeRef = useTemplateRef('timeRef')
const date = computed(() => new Date(page.value.lastUpdated!))
const isoDatetime = computed(() => date.value.toISOString())
const datetime = shallowRef('')
// set time on mounted hook to avoid hydration mismatch due to
// potential differences in timezones of the server and clients
onMounted(() => {
watchEffect(() => {
const lang = theme.value.lastUpdated?.formatOptions?.forceLocale
? pageLang.value
: browserLang.value
datetime.value = new Intl.DateTimeFormat(
lang,
theme.value.lastUpdated?.formatOptions ?? {
dateStyle: 'medium',
timeStyle: 'medium'
}
).format(date.value)
if (lang && pageLang.value !== lang) {
timeRef.value?.setAttribute('lang', lang)
} else {
timeRef.value?.removeAttribute('lang')
}
})
})
</script>
<template>
<p class="VPLastUpdated">
{{ theme.lastUpdated?.text || theme.lastUpdatedText || 'Last updated' }}:
<time ref="timeRef" :datetime="isoDatetime">{{ datetime }}</time>
</p>
</template>
<style scoped>
.VPLastUpdated {
line-height: 24px;
font-size: 14px;
font-weight: 500;
color: var(--vp-c-text-2);
}
@media (min-width: 640px) {
.VPLastUpdated {
line-height: 32px;
font-size: 14px;
font-weight: 500;
}
}
</style>

View File

@@ -0,0 +1,55 @@
<script setup lang="ts">
import type { DefaultTheme } from 'vitepress/theme'
defineProps<{
headers: DefaultTheme.OutlineItem[]
root?: boolean
}>()
</script>
<template>
<ul class="VPDocOutlineItem" :class="root ? 'root' : 'nested'">
<li v-for="{ children, link, title } in headers">
<a class="outline-link" :href="link" :title>
{{ title }}
</a>
<template v-if="children?.length">
<VPDocOutlineItem :headers="children" />
</template>
</li>
</ul>
</template>
<style scoped>
.root {
position: relative;
z-index: 1;
}
.nested {
padding-right: 16px;
padding-left: 16px;
}
.outline-link {
display: block;
line-height: 32px;
font-size: 14px;
font-weight: 400;
color: var(--vp-c-text-2);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: color 0.5s;
}
.outline-link:hover,
.outline-link.active {
color: var(--vp-c-text-1);
transition: color 0.25s;
}
.outline-link.nested {
padding-left: 13px;
}
</style>

View File

@@ -0,0 +1,123 @@
<script setup lang="ts">
import type { DefaultTheme } from 'vitepress/theme'
import VPImage from './VPImage.vue'
import VPLink from './VPLink.vue'
defineProps<{
icon?: DefaultTheme.FeatureIcon
title: string
details?: string
link?: string
linkText?: string
rel?: string
target?: string
}>()
</script>
<template>
<VPLink
class="VPFeature"
:href="link"
:rel
:target
:no-icon="true"
:tag="link ? 'a' : 'div'"
>
<article class="box">
<div v-if="typeof icon === 'object' && icon.wrap" class="icon">
<VPImage
:image="icon"
:alt="icon.alt"
:height="icon.height || 48"
:width="icon.width || 48"
/>
</div>
<VPImage
v-else-if="typeof icon === 'object'"
:image="icon"
:alt="icon.alt"
:height="icon.height || 48"
:width="icon.width || 48"
/>
<div v-else-if="icon" class="icon" v-html="icon"></div>
<h2 class="title" v-html="title"></h2>
<p v-if="details" class="details" v-html="details"></p>
<div v-if="linkText" class="link-text">
<p class="link-text-value">
{{ linkText }} <span class="vpi-arrow-right link-text-icon" />
</p>
</div>
</article>
</VPLink>
</template>
<style scoped>
.VPFeature {
display: block;
border: 1px solid var(--vp-c-bg-soft);
border-radius: 12px;
height: 100%;
background-color: var(--vp-c-bg-soft);
transition: border-color 0.25s, background-color 0.25s;
}
.VPFeature.link:hover {
border-color: var(--vp-c-brand-1);
}
.box {
display: flex;
flex-direction: column;
padding: 24px;
height: 100%;
}
.box > :deep(.VPImage) {
margin-bottom: 20px;
}
.icon {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 20px;
border-radius: 6px;
background-color: var(--vp-c-default-soft);
width: 48px;
height: 48px;
font-size: 24px;
transition: background-color 0.25s;
}
.title {
line-height: 24px;
font-size: 16px;
font-weight: 600;
}
.details {
flex-grow: 1;
padding-top: 8px;
line-height: 24px;
font-size: 14px;
font-weight: 500;
color: var(--vp-c-text-2);
}
.link-text {
padding-top: 8px;
}
.link-text-value {
display: flex;
align-items: center;
font-size: 14px;
font-weight: 500;
color: var(--vp-c-brand-1);
}
.link-text-icon {
margin-left: 6px;
}
</style>

View File

@@ -0,0 +1,121 @@
<script setup lang="ts">
import type { DefaultTheme } from 'vitepress/theme'
import { computed } from 'vue'
import VPFeature from './VPFeature.vue'
export interface Feature {
icon?: DefaultTheme.FeatureIcon
title: string
details: string
link?: string
linkText?: string
rel?: string
target?: string
}
const props = defineProps<{
features: Feature[]
}>()
const grid = computed(() => {
const length = props.features.length
if (!length) {
return
} else if (length === 2) {
return 'grid-2'
} else if (length === 3) {
return 'grid-3'
} else if (length % 3 === 0) {
return 'grid-6'
} else if (length > 3) {
return 'grid-4'
}
})
</script>
<template>
<div v-if="features" class="VPFeatures">
<div class="container">
<div class="items">
<div
v-for="feature in features"
:key="feature.title"
class="item"
:class="[grid]"
>
<VPFeature
:icon="feature.icon"
:title="feature.title"
:details="feature.details"
:link="feature.link"
:link-text="feature.linkText"
:rel="feature.rel"
:target="feature.target"
/>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.VPFeatures {
position: relative;
padding: 0 24px;
}
@media (min-width: 640px) {
.VPFeatures {
padding: 0 48px;
}
}
@media (min-width: 960px) {
.VPFeatures {
padding: 0 64px;
}
}
.container {
margin: 0 auto;
max-width: 1152px;
}
.items {
display: flex;
flex-wrap: wrap;
margin: -8px;
}
.item {
padding: 8px;
width: 100%;
}
@media (min-width: 640px) {
.item.grid-2,
.item.grid-4,
.item.grid-6 {
width: calc(100% / 2);
}
}
@media (min-width: 768px) {
.item.grid-2,
.item.grid-4 {
width: calc(100% / 2);
}
.item.grid-3,
.item.grid-6 {
width: calc(100% / 3);
}
}
@media (min-width: 960px) {
.item.grid-4 {
width: calc(100% / 4);
}
}
</style>

View File

@@ -0,0 +1,137 @@
<script lang="ts" setup generic="T extends DefaultTheme.NavItem">
import type { DefaultTheme } from 'vitepress/theme'
import { ref } from 'vue'
import { useFlyout } from '../composables/flyout'
import VPMenu from './VPMenu.vue'
defineProps<{
icon?: string
button?: string
label?: string
items?: T[]
}>()
const open = ref(false)
const el = ref<HTMLElement>()
useFlyout({ el, onBlur })
function onBlur() {
open.value = false
}
</script>
<template>
<div
class="VPFlyout"
ref="el"
@mouseenter="open = true"
@mouseleave="open = false"
>
<button
type="button"
class="button"
aria-haspopup="true"
:aria-expanded="open"
:aria-label="label"
@click="open = !open"
>
<span v-if="button || icon" class="text">
<span v-if="icon" :class="[icon, 'option-icon']" />
<span v-if="button" v-html="button"></span>
<span class="vpi-chevron-down text-icon" />
</span>
<span v-else class="vpi-more-horizontal icon" />
</button>
<div class="menu">
<VPMenu :items>
<slot />
</VPMenu>
</div>
</div>
</template>
<style scoped>
.VPFlyout {
position: relative;
}
.VPFlyout:hover {
color: var(--vp-c-brand-1);
transition: color 0.25s;
}
.VPFlyout:hover .text {
color: var(--vp-c-text-2);
}
.VPFlyout:hover .icon {
fill: var(--vp-c-text-2);
}
.VPFlyout.active .text {
color: var(--vp-c-brand-1);
}
.VPFlyout.active:hover .text {
color: var(--vp-c-brand-2);
}
.button[aria-expanded="false"] + .menu {
opacity: 0;
visibility: hidden;
transform: translateY(0);
}
.VPFlyout:hover .menu,
.button[aria-expanded="true"] + .menu {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.button {
display: flex;
align-items: center;
padding: 0 12px;
height: var(--vp-nav-height);
color: var(--vp-c-text-1);
transition: color 0.5s;
}
.text {
display: flex;
align-items: center;
line-height: var(--vp-nav-height);
font-size: 14px;
font-weight: 500;
color: var(--vp-c-text-1);
transition: color 0.25s;
}
.option-icon {
margin-right: 0px;
font-size: 16px;
}
.text-icon {
margin-left: 4px;
font-size: 14px;
}
.icon {
font-size: 20px;
transition: fill 0.25s;
}
.menu {
position: absolute;
top: calc(var(--vp-nav-height) / 2 + 20px);
right: 0;
opacity: 0;
visibility: hidden;
transition: opacity 0.25s, visibility 0.25s, transform 0.25s;
}
</style>

View File

@@ -0,0 +1,60 @@
<script setup lang="ts">
import { useData } from '../composables/data'
import { useLayout } from '../composables/layout'
const { theme, frontmatter } = useData()
const { hasSidebar } = useLayout()
</script>
<template>
<footer v-if="theme.footer && frontmatter.footer !== false" class="VPFooter" :class="{ 'has-sidebar': hasSidebar }">
<div class="container">
<p v-if="theme.footer.message" class="message" v-html="theme.footer.message"></p>
<p v-if="theme.footer.copyright" class="copyright" v-html="theme.footer.copyright"></p>
</div>
</footer>
</template>
<style scoped>
.VPFooter {
position: relative;
z-index: var(--vp-z-index-footer);
border-top: 1px solid var(--vp-c-gutter);
padding: 32px 24px;
background-color: var(--vp-c-bg);
}
.VPFooter.has-sidebar {
display: none;
}
.VPFooter :deep(a) {
text-decoration-line: underline;
text-underline-offset: 2px;
transition: color 0.25s;
}
.VPFooter :deep(a:hover) {
color: var(--vp-c-text-1);
}
@media (min-width: 768px) {
.VPFooter {
padding: 32px;
}
}
.container {
margin: 0 auto;
max-width: var(--vp-layout-max-width);
text-align: center;
}
.message,
.copyright {
line-height: 24px;
font-size: 14px;
font-weight: 500;
color: var(--vp-c-text-2);
}
</style>

View File

@@ -0,0 +1,346 @@
<script setup lang="ts">
import type { DefaultTheme } from 'vitepress/theme'
import { inject } from 'vue'
import { layoutInfoInjectionKey } from '../composables/layout'
import VPButton from './VPButton.vue'
import VPImage from './VPImage.vue'
export interface HeroAction {
theme?: 'brand' | 'alt'
text: string
link: string
target?: string
rel?: string
}
defineProps<{
name?: string
text?: string
tagline?: string
image?: DefaultTheme.ThemeableImage
actions?: HeroAction[]
}>()
const { heroImageSlotExists } = inject(layoutInfoInjectionKey)!
</script>
<template>
<div class="VPHero" :class="{ 'has-image': image || heroImageSlotExists }">
<div class="container">
<div class="main">
<slot name="home-hero-info-before" />
<slot name="home-hero-info">
<h1 class="heading">
<span v-if="name" v-html="name" class="name clip"></span>
<span v-if="text" v-html="text" class="text"></span>
</h1>
<p v-if="tagline" v-html="tagline" class="tagline"></p>
</slot>
<slot name="home-hero-info-after" />
<div v-if="actions" class="actions">
<div v-for="action in actions" :key="action.link" class="action">
<VPButton
tag="a"
size="medium"
:theme="action.theme"
:text="action.text"
:href="action.link"
:target="action.target"
:rel="action.rel"
/>
</div>
</div>
<slot name="home-hero-actions-after" />
</div>
<div v-if="image || heroImageSlotExists" class="image">
<div class="image-container">
<div class="image-bg" />
<slot name="home-hero-image">
<VPImage v-if="image" class="image-src" :image />
</slot>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.VPHero {
margin-top: calc((var(--vp-nav-height) + var(--vp-layout-top-height, 0px)) * -1);
padding: calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 48px) 24px 48px;
}
@media (min-width: 640px) {
.VPHero {
padding: calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 80px) 48px 64px;
}
}
@media (min-width: 960px) {
.VPHero {
padding: calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 80px) 64px 64px;
}
}
.container {
display: flex;
flex-direction: column;
margin: 0 auto;
max-width: 1152px;
}
@media (min-width: 960px) {
.container {
flex-direction: row;
}
}
.main {
position: relative;
z-index: 10;
order: 2;
flex-grow: 1;
flex-shrink: 0;
}
.VPHero.has-image .container {
text-align: center;
}
@media (min-width: 960px) {
.VPHero.has-image .container {
text-align: left;
}
}
@media (min-width: 960px) {
.main {
order: 1;
width: calc((100% / 3) * 2);
}
.VPHero.has-image .main {
max-width: 592px;
}
}
.heading {
display: flex;
flex-direction: column;
}
.name,
.text {
width: fit-content;
max-width: 392px;
letter-spacing: -0.4px;
line-height: 40px;
font-size: 32px;
font-weight: 700;
white-space: pre-wrap;
}
.VPHero.has-image .name,
.VPHero.has-image .text {
margin: 0 auto;
}
.name {
color: var(--vp-home-hero-name-color);
}
.clip {
background: var(--vp-home-hero-name-background);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: var(--vp-home-hero-name-color);
}
@media (min-width: 640px) {
.name,
.text {
max-width: 576px;
line-height: 56px;
font-size: 48px;
}
}
@media (min-width: 960px) {
.name,
.text {
line-height: 64px;
font-size: 56px;
}
.VPHero.has-image .name,
.VPHero.has-image .text {
margin: 0;
}
}
.tagline {
padding-top: 8px;
max-width: 392px;
line-height: 28px;
font-size: 18px;
font-weight: 500;
white-space: pre-wrap;
color: var(--vp-c-text-2);
}
.VPHero.has-image .tagline {
margin: 0 auto;
}
@media (min-width: 640px) {
.tagline {
padding-top: 12px;
max-width: 576px;
line-height: 32px;
font-size: 20px;
}
}
@media (min-width: 960px) {
.tagline {
line-height: 36px;
font-size: 24px;
}
.VPHero.has-image .tagline {
margin: 0;
}
}
.actions {
display: flex;
flex-wrap: wrap;
margin: -6px;
padding-top: 24px;
}
.VPHero.has-image .actions {
justify-content: center;
}
@media (min-width: 640px) {
.actions {
padding-top: 32px;
}
}
@media (min-width: 960px) {
.VPHero.has-image .actions {
justify-content: flex-start;
}
}
.action {
flex-shrink: 0;
padding: 6px;
}
.image {
order: 1;
margin: -76px -24px -48px;
}
@media (min-width: 640px) {
.image {
margin: -108px -24px -48px;
}
}
@media (min-width: 960px) {
.image {
flex-grow: 1;
order: 2;
margin: 0;
min-height: 100%;
}
}
.image-container {
position: relative;
margin: 0 auto;
width: 320px;
height: 320px;
}
@media (min-width: 640px) {
.image-container {
width: 392px;
height: 392px;
}
}
@media (min-width: 960px) {
.image-container {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
/*rtl:ignore*/
transform: translate(-32px, -32px);
}
}
.image-bg {
position: absolute;
top: 50%;
/*rtl:ignore*/
left: 50%;
border-radius: 50%;
width: 192px;
height: 192px;
background-image: var(--vp-home-hero-image-background-image);
filter: var(--vp-home-hero-image-filter);
/*rtl:ignore*/
transform: translate(-50%, -50%);
}
@media (min-width: 640px) {
.image-bg {
width: 256px;
height: 256px;
}
}
@media (min-width: 960px) {
.image-bg {
width: 320px;
height: 320px;
}
}
:deep(.image-src) {
position: absolute;
top: 50%;
/*rtl:ignore*/
left: 50%;
max-width: 192px;
max-height: 192px;
width: 100%;
height: 100%;
object-fit: contain;
/*rtl:ignore*/
transform: translate(-50%, -50%);
}
@media (min-width: 640px) {
:deep(.image-src) {
max-width: 256px;
max-height: 256px;
}
}
@media (min-width: 960px) {
:deep(.image-src) {
max-width: 320px;
max-height: 320px;
}
}
</style>

View File

@@ -0,0 +1,47 @@
<script setup lang="ts">
import VPHomeHero from './VPHomeHero.vue'
import VPHomeFeatures from './VPHomeFeatures.vue'
import VPHomeContent from './VPHomeContent.vue'
import { useData } from '../composables/data'
const { frontmatter, theme } = useData()
</script>
<template>
<div
class="VPHome"
:class="{
'external-link-icon-enabled': theme.externalLinkIcon
}">
<slot name="home-hero-before" />
<VPHomeHero>
<template #home-hero-info-before><slot name="home-hero-info-before" /></template>
<template #home-hero-info><slot name="home-hero-info" /></template>
<template #home-hero-info-after><slot name="home-hero-info-after" /></template>
<template #home-hero-actions-after><slot name="home-hero-actions-after" /></template>
<template #home-hero-image><slot name="home-hero-image" /></template>
</VPHomeHero>
<slot name="home-hero-after" />
<slot name="home-features-before" />
<VPHomeFeatures />
<slot name="home-features-after" />
<VPHomeContent v-if="frontmatter.markdownStyles !== false">
<Content />
</VPHomeContent>
<Content v-else />
</div>
</template>
<style scoped>
.VPHome {
margin-bottom: 96px;
}
@media (min-width: 768px) {
.VPHome {
margin-bottom: 128px;
}
}
</style>

View File

@@ -0,0 +1,55 @@
<script setup lang="ts">
import { useWindowSize } from '@vueuse/core'
const { width: vw } = useWindowSize({
initialWidth: 0,
includeScrollbar: false
})
</script>
<template>
<div
class="vp-doc container"
:style="vw ? { '--vp-offset': `calc(50% - ${vw / 2}px)` } : {}"
>
<slot />
</div>
</template>
<style scoped>
.container {
margin: auto;
width: 100%;
max-width: 1280px;
padding: 0 24px;
}
@media (min-width: 640px) {
.container {
padding: 0 48px;
}
}
@media (min-width: 960px) {
.container {
width: 100%;
padding: 0 64px;
}
}
.vp-doc :deep(.VPHomeSponsors),
.vp-doc :deep(.VPTeamPage) {
margin-left: var(--vp-offset, calc(50% - 50vw));
margin-right: var(--vp-offset, calc(50% - 50vw));
}
.vp-doc :deep(.VPHomeSponsors h2) {
border-top: none;
letter-spacing: normal;
}
.vp-doc :deep(.VPHomeSponsors a),
.vp-doc :deep(.VPTeamPage a) {
text-decoration: none;
}
</style>

View File

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

View File

@@ -0,0 +1,24 @@
<script setup lang="ts">
import { useData } from '../composables/data'
import VPHero from './VPHero.vue'
const { frontmatter: fm } = useData()
</script>
<template>
<VPHero
v-if="fm.hero"
class="VPHomeHero"
:name="fm.hero.name"
:text="fm.hero.text"
:tagline="fm.hero.tagline"
:image="fm.hero.image"
:actions="fm.hero.actions"
>
<template #home-hero-info-before><slot name="home-hero-info-before" /></template>
<template #home-hero-info><slot name="home-hero-info" /></template>
<template #home-hero-info-after><slot name="home-hero-info-after" /></template>
<template #home-hero-actions-after><slot name="home-hero-actions-after" /></template>
<template #home-hero-image><slot name="home-hero-image" /></template>
</VPHero>
</template>

View File

@@ -0,0 +1,116 @@
<script setup lang="ts">
import VPButton from './VPButton.vue'
import VPSponsors from './VPSponsors.vue'
export interface Sponsors {
tier: string
size?: 'medium' | 'big'
items: Sponsor[]
}
export interface Sponsor {
name: string
img: string
url: string
}
interface Props {
message?: string
actionText?: string
actionLink?: string
data: Sponsors[]
}
withDefaults(defineProps<Props>(), {
actionText: 'Become a sponsor'
})
</script>
<template>
<section class="VPHomeSponsors">
<div class="container">
<div class="header">
<div class="love">
<span class="vpi-heart icon" />
</div>
<h2 v-if="message" class="message">{{ message }}</h2>
</div>
<div class="sponsors">
<VPSponsors :data />
</div>
<div v-if="actionLink" class="action">
<VPButton theme="sponsor" :text="actionText" :href="actionLink" />
</div>
</div>
</section>
</template>
<style scoped>
.VPHomeSponsors {
border-top: 1px solid var(--vp-c-gutter);
padding-top: 88px !important;
}
.VPHomeSponsors {
margin: 96px 0;
}
@media (min-width: 768px) {
.VPHomeSponsors {
margin: 128px 0;
}
}
.VPHomeSponsors {
padding: 0 24px;
}
@media (min-width: 768px) {
.VPHomeSponsors {
padding: 0 48px;
}
}
@media (min-width: 960px) {
.VPHomeSponsors {
padding: 0 64px;
}
}
.container {
margin: 0 auto;
max-width: 1152px;
}
.love {
margin: 0 auto;
width: fit-content;
font-size: 28px;
color: var(--vp-c-text-3);
}
.icon {
display: inline-block;
}
.message {
margin: 0 auto;
padding-top: 10px;
max-width: 320px;
text-align: center;
line-height: 24px;
font-size: 16px;
font-weight: 500;
color: var(--vp-c-text-2);
}
.sponsors {
padding-top: 32px;
}
.action {
padding-top: 40px;
text-align: center;
}
</style>

View File

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

View File

@@ -0,0 +1,37 @@
<script lang="ts" setup>
import { computed } from 'vue'
import { normalizeLink } from '../support/utils'
import { EXTERNAL_URL_RE } from '../../shared'
const props = defineProps<{
tag?: string
href?: string
noIcon?: boolean
target?: string
rel?: string
}>()
const tag = computed(() => props.tag ?? (props.href ? 'a' : 'span'))
const isExternal = computed(
() =>
(props.href && EXTERNAL_URL_RE.test(props.href)) ||
props.target === '_blank'
)
</script>
<template>
<component
:is="tag"
class="VPLink"
:class="{
link: href,
'vp-external-link-icon': isExternal,
'no-icon': noIcon
}"
:href="href ? normalizeLink(href) : undefined"
:target="target ?? (isExternal ? '_blank' : undefined)"
:rel="rel ?? (isExternal ? 'noreferrer' : undefined)"
>
<slot />
</component>
</template>

View File

@@ -0,0 +1,144 @@
<script lang="ts" setup>
import { useWindowScroll } from '@vueuse/core'
import { computed, onMounted, ref } from 'vue'
import { useData } from '../composables/data'
import { useLayout } from '../composables/layout'
import VPLocalNavOutlineDropdown from './VPLocalNavOutlineDropdown.vue'
defineProps<{
open: boolean
}>()
defineEmits<{
(e: 'open-menu'): void
}>()
const { theme } = useData()
const { isHome, hasSidebar, headers, hasLocalNav } = useLayout()
const { y } = useWindowScroll()
const navHeight = ref(0)
onMounted(() => {
navHeight.value = parseInt(
getComputedStyle(document.documentElement).getPropertyValue(
'--vp-nav-height'
)
)
})
const classes = computed(() => {
return {
VPLocalNav: true,
'has-sidebar': hasSidebar.value,
empty: !hasLocalNav.value,
fixed: !hasLocalNav.value && !hasSidebar.value
}
})
</script>
<template>
<div
v-if="!isHome && (hasLocalNav || hasSidebar || y >= navHeight)"
:class="classes"
>
<div class="container">
<button
v-if="hasSidebar"
class="menu"
:aria-expanded="open"
aria-controls="VPSidebarNav"
@click="$emit('open-menu')"
>
<span class="vpi-align-left menu-icon"></span>
<span class="menu-text">
{{ theme.sidebarMenuLabel || 'Menu' }}
</span>
</button>
<VPLocalNavOutlineDropdown :headers :navHeight />
</div>
</div>
</template>
<style scoped>
.VPLocalNav {
position: sticky;
top: 0;
/*rtl:ignore*/
left: 0;
z-index: var(--vp-z-index-local-nav);
border-bottom: 1px solid var(--vp-c-gutter);
padding-top: var(--vp-layout-top-height, 0px);
width: 100%;
background-color: var(--vp-local-nav-bg-color);
}
.VPLocalNav.fixed {
position: fixed;
}
@media (min-width: 960px) {
.VPLocalNav {
top: var(--vp-nav-height);
}
.VPLocalNav.has-sidebar {
padding-left: var(--vp-sidebar-width);
}
.VPLocalNav.empty {
display: none;
}
}
@media (min-width: 1280px) {
.VPLocalNav {
display: none;
}
}
.container {
display: flex;
justify-content: space-between;
align-items: center;
}
.menu {
display: flex;
align-items: center;
line-height: 24px;
font-size: 12px;
font-weight: 500;
color: var(--vp-c-text-2);
transition: color 0.5s;
}
.menu:hover {
color: var(--vp-c-text-1);
transition: color 0.25s;
}
@media (min-width: 960px) {
.menu {
display: none;
}
}
.menu-icon {
margin-right: 8px;
font-size: 14px;
}
.menu,
:deep(.VPLocalNavOutlineDropdown > button) {
padding: 12px 24px 11px;
}
@media (min-width: 768px) {
.menu,
:deep(.VPLocalNavOutlineDropdown > button) {
padding: 12px 32px 11px;
}
}
</style>

View File

@@ -0,0 +1,192 @@
<script setup lang="ts">
import { onKeyStroke } from '@vueuse/core'
import { onContentUpdated } from 'vitepress'
import type { DefaultTheme } from 'vitepress/theme'
import { nextTick, ref, watch } from 'vue'
import { useData } from '../composables/data'
import { resolveTitle } from '../composables/outline'
import VPDocOutlineItem from './VPDocOutlineItem.vue'
const props = defineProps<{
headers: DefaultTheme.OutlineItem[]
navHeight: number
}>()
const { theme } = useData()
const open = ref(false)
const vh = ref(0)
const main = ref<HTMLDivElement>()
const items = ref<HTMLDivElement>()
function closeOnClickOutside(e: Event) {
if (!main.value?.contains(e.target as Node)) {
open.value = false
}
}
watch(open, (value) => {
if (value) {
document.addEventListener('click', closeOnClickOutside)
return
}
document.removeEventListener('click', closeOnClickOutside)
})
onKeyStroke('Escape', () => {
open.value = false
})
onContentUpdated(() => {
open.value = false
})
function toggle() {
open.value = !open.value
vh.value = window.innerHeight + Math.min(window.scrollY - props.navHeight, 0)
}
function onItemClick(e: Event) {
if ((e.target as HTMLElement).classList.contains('outline-link')) {
// disable animation on hash navigation when page jumps
if (items.value) {
items.value.style.transition = 'none'
}
nextTick(() => {
open.value = false
})
}
}
function scrollToTop() {
open.value = false
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
}
</script>
<template>
<div
class="VPLocalNavOutlineDropdown"
:style="{ '--vp-vh': vh + 'px' }"
ref="main"
>
<button @click="toggle" :class="{ open }" v-if="headers.length > 0">
<span class="menu-text">{{ resolveTitle(theme) }}</span>
<span class="vpi-chevron-right icon" />
</button>
<button @click="scrollToTop" v-else>
{{ theme.returnToTopLabel || 'Return to top' }}
</button>
<Transition name="flyout">
<div v-if="open" ref="items" class="items" @click="onItemClick">
<div class="header">
<a class="top-link" href="#" @click="scrollToTop">
{{ theme.returnToTopLabel || 'Return to top' }}
</a>
</div>
<div class="outline">
<VPDocOutlineItem :headers />
</div>
</div>
</Transition>
</div>
</template>
<style scoped>
.VPLocalNavOutlineDropdown button {
display: block;
font-size: 12px;
font-weight: 500;
line-height: 24px;
color: var(--vp-c-text-2);
transition: color 0.5s;
position: relative;
}
.VPLocalNavOutlineDropdown button:hover {
color: var(--vp-c-text-1);
transition: color 0.25s;
}
.VPLocalNavOutlineDropdown button.open {
color: var(--vp-c-text-1);
}
.icon {
display: inline-block;
vertical-align: middle;
margin-left: 2px;
font-size: 14px;
transform: rotate(0) /*rtl:rotate(180deg)*/;
transition: transform 0.25s;
}
@media (min-width: 960px) {
.VPLocalNavOutlineDropdown button {
font-size: 14px;
}
.icon {
font-size: 16px;
}
}
.open > .icon {
/*rtl:ignore*/
transform: rotate(90deg);
}
.items {
position: absolute;
top: 40px;
right: 16px;
left: 16px;
display: grid;
gap: 1px;
border: 1px solid var(--vp-c-border);
border-radius: 8px;
background-color: var(--vp-c-gutter);
max-height: calc(var(--vp-vh, 100vh) - 86px);
overflow: hidden auto;
box-shadow: var(--vp-shadow-3);
}
@media (min-width: 960px) {
.items {
right: auto;
left: calc(var(--vp-sidebar-width) + 32px);
width: 320px;
}
}
.header {
background-color: var(--vp-c-bg-soft);
}
.top-link {
display: block;
padding: 0 16px;
line-height: 48px;
font-size: 14px;
font-weight: 500;
color: var(--vp-c-brand-1);
}
.outline {
padding: 8px 0;
background-color: var(--vp-c-bg-soft);
}
.flyout-enter-active {
transition: all 0.2s ease-out;
}
.flyout-leave-active {
transition: all 0.15s ease-in;
}
.flyout-enter-from,
.flyout-leave-to {
opacity: 0;
transform: translateY(-16px);
}
</style>

View File

@@ -0,0 +1,877 @@
<script lang="ts" setup>
import localSearchIndex from '@localSearchIndex'
import {
computedAsync,
debouncedWatch,
onKeyStroke,
useEventListener,
useLocalStorage,
useScrollLock,
useSessionStorage
} from '@vueuse/core'
import { useFocusTrap } from '@vueuse/integrations/useFocusTrap'
import Mark from 'mark.js/src/vanilla.js'
import MiniSearch, { type SearchResult } from 'minisearch'
import { dataSymbol, inBrowser, useRouter } from 'vitepress'
import {
computed,
createApp,
markRaw,
nextTick,
onBeforeUnmount,
onMounted,
ref,
shallowRef,
watch,
watchEffect,
type Ref
} from 'vue'
import type { ModalTranslations } from '../../../../types/local-search'
import { pathToFile } from '../../app/utils'
import { escapeRegExp } from '../../shared'
import { useData } from '../composables/data'
import { LRUCache } from '../support/lru'
import { createSearchTranslate } from '../support/translation'
const emit = defineEmits<{
(e: 'close'): void
}>()
const el = shallowRef<HTMLElement>()
const resultsEl = shallowRef<HTMLElement>()
/* Search */
const searchIndexData = shallowRef(localSearchIndex)
// hmr
if (import.meta.hot) {
import.meta.hot.accept('@localSearchIndex', (m) => {
if (m) {
searchIndexData.value = m.default
}
})
}
interface Result {
title: string
titles: string[]
text?: string
}
const vitePressData = useData()
const { activate } = useFocusTrap(el, {
immediate: true,
allowOutsideClick: true,
clickOutsideDeactivates: true,
escapeDeactivates: true
})
const { localeIndex, theme } = vitePressData
const searchIndex = computedAsync(async () =>
markRaw(
MiniSearch.loadJSON<Result>(
(await searchIndexData.value[localeIndex.value]?.())?.default,
{
fields: ['title', 'titles', 'text'],
storeFields: ['title', 'titles'],
searchOptions: {
fuzzy: 0.2,
prefix: true,
boost: { title: 4, text: 2, titles: 1 },
...(theme.value.search?.provider === 'local' &&
theme.value.search.options?.miniSearch?.searchOptions)
},
...(theme.value.search?.provider === 'local' &&
theme.value.search.options?.miniSearch?.options)
}
)
)
)
const disableQueryPersistence = computed(() => {
return (
theme.value.search?.provider === 'local' &&
theme.value.search.options?.disableQueryPersistence === true
)
})
const filterText = disableQueryPersistence.value
? ref('')
: useSessionStorage('vitepress:local-search-filter', '')
const showDetailedList = useLocalStorage(
'vitepress:local-search-detailed-list',
theme.value.search?.provider === 'local' &&
theme.value.search.options?.detailedView === true
)
const disableDetailedView = computed(() => {
return (
theme.value.search?.provider === 'local' &&
(theme.value.search.options?.disableDetailedView === true ||
theme.value.search.options?.detailedView === false)
)
})
const buttonText = computed(() => {
const options = theme.value.search?.options ?? theme.value.algolia
return (
options?.locales?.[localeIndex.value]?.translations?.button?.buttonText ||
options?.translations?.button?.buttonText ||
'Search'
)
})
watchEffect(() => {
if (disableDetailedView.value) {
showDetailedList.value = false
}
})
const results: Ref<(SearchResult & Result)[]> = shallowRef([])
const enableNoResults = ref(false)
watch(filterText, () => {
enableNoResults.value = false
})
const mark = computedAsync(async () => {
if (!resultsEl.value) return
return markRaw(new Mark(resultsEl.value))
}, null)
const cache = new LRUCache<string, Map<string, string>>(16) // 16 files
debouncedWatch(
() => [searchIndex.value, filterText.value, showDetailedList.value] as const,
async ([index, filterTextValue, showDetailedListValue], old, onCleanup) => {
if (old?.[0] !== index) {
// in case of hmr
cache.clear()
}
let canceled = false
onCleanup(() => {
canceled = true
})
if (!index) return
// Search
results.value = index
.search(filterTextValue)
.slice(0, 16) as (SearchResult & Result)[]
enableNoResults.value = true
// Highlighting
const mods = showDetailedListValue
? await Promise.all(results.value.map((r) => fetchExcerpt(r.id)))
: []
if (canceled) return
for (const { id, mod } of mods) {
const mapId = id.slice(0, id.indexOf('#'))
let map = cache.get(mapId)
if (map) continue
map = new Map()
cache.set(mapId, map)
const comp = mod.default ?? mod
if (comp?.render || comp?.setup) {
const app = createApp(comp)
// Silence warnings about missing components
app.config.warnHandler = () => {}
app.provide(dataSymbol, vitePressData)
Object.defineProperties(app.config.globalProperties, {
$frontmatter: {
get() {
return vitePressData.frontmatter.value
}
},
$params: {
get() {
return vitePressData.page.value.params
}
}
})
const div = document.createElement('div')
app.mount(div)
const headings = div.querySelectorAll('h1, h2, h3, h4, h5, h6')
headings.forEach((el) => {
const href = el.querySelector('a')?.getAttribute('href')
const anchor = href?.startsWith('#') && href.slice(1)
if (!anchor) return
let html = ''
while ((el = el.nextElementSibling!) && !/^h[1-6]$/i.test(el.tagName))
html += el.outerHTML
map!.set(anchor, html)
})
app.unmount()
}
if (canceled) return
}
const terms = new Set<string>()
results.value = results.value.map((r) => {
const [id, anchor] = r.id.split('#')
const map = cache.get(id)
const text = map?.get(anchor) ?? ''
for (const term in r.match) {
terms.add(term)
}
return { ...r, text }
})
await nextTick()
if (canceled) return
await new Promise((r) => {
mark.value?.unmark({
done: () => {
mark.value?.markRegExp(formMarkRegex(terms), { done: r })
}
})
})
const excerpts = el.value?.querySelectorAll('.result .excerpt') ?? []
for (const excerpt of excerpts) {
excerpt
.querySelector('mark[data-markjs="true"]')
?.scrollIntoView({ block: 'center' })
}
// FIXME: without this whole page scrolls to the bottom
resultsEl.value?.firstElementChild?.scrollIntoView({ block: 'start' })
},
{ debounce: 200, immediate: true }
)
async function fetchExcerpt(id: string) {
const file = pathToFile(id.slice(0, id.indexOf('#')))
try {
if (!file) throw new Error(`Cannot find file for id: ${id}`)
return { id, mod: await import(/*@vite-ignore*/ file) }
} catch (e) {
console.error(e)
return { id, mod: {} }
}
}
/* Search input focus */
const searchInput = ref<HTMLInputElement>()
const disableReset = computed(() => {
return filterText.value?.length <= 0
})
function focusSearchInput(select = true) {
searchInput.value?.focus()
select && searchInput.value?.select()
}
onMounted(() => {
focusSearchInput()
})
function onSearchBarClick(event: PointerEvent) {
if (event.pointerType === 'mouse') {
focusSearchInput()
}
}
/* Search keyboard selection */
const selectedIndex = ref(-1)
const disableMouseOver = ref(true)
watch(results, (r) => {
selectedIndex.value = r.length ? 0 : -1
scrollToSelectedResult()
})
function scrollToSelectedResult() {
nextTick(() => {
const selectedEl = document.querySelector('.result.selected')
selectedEl?.scrollIntoView({ block: 'nearest' })
})
}
onKeyStroke('ArrowUp', (event) => {
event.preventDefault()
selectedIndex.value--
if (selectedIndex.value < 0) {
selectedIndex.value = results.value.length - 1
}
disableMouseOver.value = true
scrollToSelectedResult()
})
onKeyStroke('ArrowDown', (event) => {
event.preventDefault()
selectedIndex.value++
if (selectedIndex.value >= results.value.length) {
selectedIndex.value = 0
}
disableMouseOver.value = true
scrollToSelectedResult()
})
const router = useRouter()
onKeyStroke('Enter', (e) => {
if (e.isComposing) return
if (e.target instanceof HTMLButtonElement && e.target.type !== 'submit')
return
const selectedPackage = results.value[selectedIndex.value]
if (e.target instanceof HTMLInputElement && !selectedPackage) {
e.preventDefault()
return
}
if (selectedPackage) {
router.go(selectedPackage.id)
emit('close')
}
})
onKeyStroke('Escape', () => {
emit('close')
})
// Translations
const defaultTranslations: { modal: ModalTranslations } = {
modal: {
displayDetails: 'Display detailed list',
resetButtonTitle: 'Reset search',
backButtonTitle: 'Close search',
noResultsText: 'No results for',
footer: {
selectText: 'to select',
selectKeyAriaLabel: 'enter',
navigateText: 'to navigate',
navigateUpKeyAriaLabel: 'up arrow',
navigateDownKeyAriaLabel: 'down arrow',
closeText: 'to close',
closeKeyAriaLabel: 'escape'
}
}
}
const translate = createSearchTranslate(defaultTranslations)
// Back
onMounted(() => {
// Prevents going to previous site
window.history.pushState(null, '', null)
})
useEventListener('popstate', (event) => {
event.preventDefault()
emit('close')
})
/** Lock body */
const isLocked = useScrollLock(inBrowser ? document.body : null)
onMounted(() => {
nextTick(() => {
isLocked.value = true
nextTick().then(() => activate())
})
})
onBeforeUnmount(() => {
isLocked.value = false
})
function resetSearch() {
filterText.value = ''
nextTick().then(() => focusSearchInput(false))
}
function formMarkRegex(terms: Set<string>) {
return new RegExp(
[...terms]
.sort((a, b) => b.length - a.length)
.map((term) => `(${escapeRegExp(term)})`)
.join('|'),
'gi'
)
}
function onMouseMove(e: MouseEvent) {
if (!disableMouseOver.value) return
const el = (e.target as HTMLElement)?.closest<HTMLAnchorElement>('.result')
const index = Number.parseInt(el?.dataset.index!)
if (index >= 0 && index !== selectedIndex.value) {
selectedIndex.value = index
}
disableMouseOver.value = false
}
</script>
<template>
<Teleport to="body">
<div
ref="el"
role="button"
:aria-owns="results?.length ? 'localsearch-list' : undefined"
aria-expanded="true"
aria-haspopup="listbox"
aria-labelledby="localsearch-label"
class="VPLocalSearchBox"
>
<div class="backdrop" @click="$emit('close')" />
<div class="shell">
<form
class="search-bar"
@pointerup="onSearchBarClick($event)"
@submit.prevent=""
>
<label
:title="buttonText"
id="localsearch-label"
for="localsearch-input"
>
<span aria-hidden="true" class="vpi-search search-icon local-search-icon" />
</label>
<div class="search-actions before">
<button
class="back-button"
:title="translate('modal.backButtonTitle')"
@click="$emit('close')"
>
<span class="vpi-arrow-left local-search-icon" />
</button>
</div>
<input
ref="searchInput"
v-model="filterText"
:aria-activedescendant="selectedIndex > -1 ? ('localsearch-item-' + selectedIndex) : undefined"
aria-autocomplete="both"
:aria-controls="results?.length ? 'localsearch-list' : undefined"
aria-labelledby="localsearch-label"
autocapitalize="off"
autocomplete="off"
autocorrect="off"
class="search-input"
id="localsearch-input"
enterkeyhint="go"
maxlength="64"
:placeholder="buttonText"
spellcheck="false"
type="search"
/>
<div class="search-actions">
<button
v-if="!disableDetailedView"
class="toggle-layout-button"
type="button"
:class="{ 'detailed-list': showDetailedList }"
:title="translate('modal.displayDetails')"
@click="
selectedIndex > -1 && (showDetailedList = !showDetailedList)
"
>
<span class="vpi-layout-list local-search-icon" />
</button>
<button
class="clear-button"
type="reset"
:disabled="disableReset"
:title="translate('modal.resetButtonTitle')"
@click="resetSearch"
>
<span class="vpi-delete local-search-icon" />
</button>
</div>
</form>
<ul
ref="resultsEl"
:id="results?.length ? 'localsearch-list' : undefined"
:role="results?.length ? 'listbox' : undefined"
:aria-labelledby="results?.length ? 'localsearch-label' : undefined"
class="results"
@mousemove="onMouseMove"
>
<li
v-for="(p, index) in results"
:key="p.id"
:id="'localsearch-item-' + index"
:aria-selected="selectedIndex === index ? 'true' : 'false'"
role="option"
>
<a
:href="p.id"
class="result"
:class="{
selected: selectedIndex === index
}"
:aria-label="[...p.titles, p.title].join(' > ')"
@mouseenter="!disableMouseOver && (selectedIndex = index)"
@focusin="selectedIndex = index"
@click="$emit('close')"
:data-index="index"
>
<div>
<div class="titles">
<span class="title-icon">#</span>
<span
v-for="(t, index) in p.titles"
:key="index"
class="title"
>
<span class="text" v-html="t" />
<span class="vpi-chevron-right local-search-icon" />
</span>
<span class="title main">
<span class="text" v-html="p.title" />
</span>
</div>
<div v-if="showDetailedList" class="excerpt-wrapper">
<div v-if="p.text" class="excerpt" inert>
<div class="vp-doc" v-html="p.text" />
</div>
<div class="excerpt-gradient-bottom" />
<div class="excerpt-gradient-top" />
</div>
</div>
</a>
</li>
<li
v-if="filterText && !results.length && enableNoResults"
class="no-results"
>
{{ translate('modal.noResultsText') }} "<strong>{{ filterText }}</strong
>"
</li>
</ul>
<div class="search-keyboard-shortcuts">
<span>
<kbd :aria-label="translate('modal.footer.navigateUpKeyAriaLabel')">
<span class="vpi-arrow-up navigate-icon" />
</kbd>
<kbd :aria-label="translate('modal.footer.navigateDownKeyAriaLabel')">
<span class="vpi-arrow-down navigate-icon" />
</kbd>
{{ translate('modal.footer.navigateText') }}
</span>
<span>
<kbd :aria-label="translate('modal.footer.selectKeyAriaLabel')">
<span class="vpi-corner-down-left navigate-icon" />
</kbd>
{{ translate('modal.footer.selectText') }}
</span>
<span>
<kbd :aria-label="translate('modal.footer.closeKeyAriaLabel')">esc</kbd>
{{ translate('modal.footer.closeText') }}
</span>
</div>
</div>
</div>
</Teleport>
</template>
<style scoped>
.VPLocalSearchBox {
position: fixed;
z-index: 100;
inset: 0;
display: flex;
}
.backdrop {
position: absolute;
inset: 0;
background: var(--vp-backdrop-bg-color);
transition: opacity 0.5s;
}
.shell {
position: relative;
padding: 12px;
margin: 64px auto;
display: flex;
flex-direction: column;
gap: 16px;
background: var(--vp-local-search-bg);
width: min(100vw - 60px, 900px);
height: min-content;
max-height: min(100vh - 128px, 900px);
border-radius: 6px;
}
@media (max-width: 767px) {
.shell {
margin: 0;
width: 100vw;
height: 100vh;
max-height: none;
border-radius: 0;
}
}
.search-bar {
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
display: flex;
align-items: center;
padding: 0 12px;
cursor: text;
}
@media (max-width: 767px) {
.search-bar {
padding: 0 8px;
}
}
.search-bar:focus-within {
border-color: var(--vp-c-brand-1);
}
.local-search-icon {
display: block;
font-size: 18px;
}
.navigate-icon {
display: block;
font-size: 14px;
}
.search-icon {
margin: 8px;
}
@media (max-width: 767px) {
.search-icon {
display: none;
}
}
.search-input {
padding: 6px 12px;
font-size: inherit;
width: 100%;
}
.search-input::-webkit-search-cancel-button {
display: none;
}
@media (max-width: 767px) {
.search-input {
padding: 6px 4px;
}
}
.search-actions {
display: flex;
gap: 4px;
}
@media (any-pointer: coarse) {
.search-actions {
gap: 8px;
}
}
@media (min-width: 769px) {
.search-actions.before {
display: none;
}
}
.search-actions button {
padding: 8px;
}
.search-actions button:not([disabled]):hover,
.toggle-layout-button.detailed-list {
color: var(--vp-c-brand-1);
}
.search-actions button.clear-button:disabled {
opacity: 0.37;
}
.search-keyboard-shortcuts {
font-size: 0.8rem;
opacity: 75%;
display: flex;
flex-wrap: wrap;
gap: 16px;
line-height: 14px;
}
.search-keyboard-shortcuts span {
display: flex;
align-items: center;
gap: 4px;
}
@media (max-width: 767px) {
.search-keyboard-shortcuts {
display: none;
}
}
.search-keyboard-shortcuts kbd {
background: rgba(128, 128, 128, 0.1);
border-radius: 4px;
padding: 3px 6px;
min-width: 24px;
display: inline-block;
text-align: center;
vertical-align: middle;
border: 1px solid rgba(128, 128, 128, 0.15);
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.1);
}
.results {
display: flex;
flex-direction: column;
gap: 6px;
overflow-x: hidden;
overflow-y: auto;
overscroll-behavior: contain;
}
.result {
display: flex;
align-items: center;
gap: 8px;
border-radius: 4px;
transition: none;
line-height: 1rem;
border: solid 2px var(--vp-local-search-result-border);
outline: none;
}
.result > div {
margin: 12px;
width: 100%;
overflow: hidden;
}
@media (max-width: 767px) {
.result > div {
margin: 8px;
}
}
.titles {
display: flex;
flex-wrap: wrap;
gap: 4px;
position: relative;
z-index: 1001;
padding: 2px 0;
}
.title {
display: flex;
align-items: center;
gap: 4px;
}
.title.main {
font-weight: 500;
}
.title-icon {
opacity: 0.5;
font-weight: 500;
color: var(--vp-c-brand-1);
}
.title svg {
opacity: 0.5;
}
.result.selected {
--vp-local-search-result-bg: var(--vp-local-search-result-selected-bg);
border-color: var(--vp-local-search-result-selected-border);
}
.excerpt-wrapper {
position: relative;
}
.excerpt {
opacity: 50%;
pointer-events: none;
max-height: 140px;
overflow: hidden;
position: relative;
margin-top: 4px;
}
.result.selected .excerpt {
opacity: 1;
}
.excerpt :deep(*) {
font-size: 0.8rem !important;
line-height: 130% !important;
}
.titles :deep(mark),
.excerpt :deep(mark) {
background-color: var(--vp-local-search-highlight-bg);
color: var(--vp-local-search-highlight-text);
border-radius: 2px;
padding: 0 2px;
}
.excerpt :deep(.vp-code-group) .tabs {
display: none;
}
.excerpt :deep(.vp-code-group) div[class*='language-'] {
border-radius: 8px !important;
}
.excerpt-gradient-bottom {
position: absolute;
bottom: -1px;
left: 0;
width: 100%;
height: 8px;
background: linear-gradient(transparent, var(--vp-local-search-result-bg));
z-index: 1000;
}
.excerpt-gradient-top {
position: absolute;
top: -1px;
left: 0;
width: 100%;
height: 8px;
background: linear-gradient(var(--vp-local-search-result-bg), transparent);
z-index: 1000;
}
.result.selected .titles,
.result.selected .title-icon {
color: var(--vp-c-brand-1) !important;
}
.no-results {
font-size: 0.9rem;
text-align: center;
padding: 12px;
}
svg {
flex: none;
}
</style>

View File

@@ -0,0 +1,78 @@
<script lang="ts" setup generic="T extends DefaultTheme.NavItem">
import type { DefaultTheme } from 'vitepress/theme'
import VPMenuLink from './VPMenuLink.vue'
import VPMenuGroup from './VPMenuGroup.vue'
defineProps<{
items?: T[]
}>()
</script>
<template>
<div class="VPMenu">
<div v-if="items" class="items">
<template v-for="item in items" :key="JSON.stringify(item)">
<VPMenuLink v-if="'link' in item" :item />
<component
v-else-if="'component' in item"
:is="item.component"
v-bind="item.props"
/>
<VPMenuGroup v-else :text="item.text" :items="item.items" />
</template>
</div>
<slot />
</div>
</template>
<style scoped>
.VPMenu {
border-radius: 12px;
padding: 12px;
min-width: 128px;
border: 1px solid var(--vp-c-divider);
background-color: var(--vp-c-bg-elv);
box-shadow: var(--vp-shadow-3);
transition: background-color 0.5s;
max-height: calc(100vh - var(--vp-nav-height));
overflow-y: auto;
}
.VPMenu :deep(.group) {
margin: 0 -12px;
padding: 0 12px 12px;
}
.VPMenu :deep(.group + .group) {
border-top: 1px solid var(--vp-c-divider);
padding: 11px 12px 12px;
}
.VPMenu :deep(.group:last-child) {
padding-bottom: 0;
}
.VPMenu :deep(.group + .item) {
border-top: 1px solid var(--vp-c-divider);
padding: 11px 16px 0;
}
.VPMenu :deep(.item) {
padding: 0 16px;
white-space: nowrap;
}
.VPMenu :deep(.label) {
flex-grow: 1;
line-height: 28px;
font-size: 12px;
font-weight: 500;
color: var(--vp-c-text-2);
transition: color 0.5s;
}
.VPMenu :deep(.action) {
padding-left: 24px;
}
</style>

View File

@@ -0,0 +1,48 @@
<script lang="ts" setup generic="T extends (DefaultTheme.NavItemComponent | DefaultTheme.NavItemChildren | DefaultTheme.NavItemWithLink)">
import type { DefaultTheme } from 'vitepress/theme'
import VPMenuLink from './VPMenuLink.vue'
defineProps<{
text?: string
items: T[]
}>()
</script>
<template>
<div class="VPMenuGroup">
<p v-if="text" class="title">{{ text }}</p>
<template v-for="item in items" :key="JSON.stringify(item)">
<VPMenuLink v-if="'link' in item" :item />
</template>
</div>
</template>
<style scoped>
.VPMenuGroup {
margin: 12px -12px 0;
border-top: 1px solid var(--vp-c-divider);
padding: 12px 12px 0;
}
.VPMenuGroup:first-child {
margin-top: 0;
border-top: 0;
padding-top: 0;
}
.VPMenuGroup + .VPMenuGroup {
margin-top: 12px;
border-top: 1px solid var(--vp-c-divider);
}
.title {
padding: 0 12px;
line-height: 32px;
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-2);
white-space: nowrap;
transition: color 0.25s;
}
</style>

View File

@@ -0,0 +1,74 @@
<script lang="ts" setup generic="T extends DefaultTheme.NavItemWithLink">
import type { DefaultTheme } from 'vitepress/theme'
import { computed } from 'vue'
import { useData } from '../composables/data'
import { isActive } from '../../shared'
import VPLink from './VPLink.vue'
const props = defineProps<{
item: T
}>()
const { page } = useData()
const href = computed(() =>
typeof props.item.link === 'function'
? props.item.link(page.value)
: props.item.link
)
defineOptions({ inheritAttrs: false })
</script>
<template>
<div class="VPMenuLink">
<VPLink
v-bind="$attrs"
:class="{
active: isActive(
page.relativePath,
item.activeMatch || href,
!!item.activeMatch
)
}"
:href
:target="item.target"
:rel="item.rel"
:no-icon="item.noIcon"
>
<span v-html="item.text"></span>
</VPLink>
</div>
</template>
<style scoped>
.VPMenuGroup + .VPMenuLink {
margin: 12px -12px 0;
border-top: 1px solid var(--vp-c-divider);
padding: 12px 12px 0;
}
.link {
display: block;
border-radius: 6px;
padding: 0 12px;
line-height: 32px;
font-size: 14px;
font-weight: 500;
color: var(--vp-c-text-1);
text-align: left;
white-space: nowrap;
transition:
background-color 0.25s,
color 0.25s;
}
.link:hover {
color: var(--vp-c-brand-1);
background-color: var(--vp-c-default-soft);
}
.link.active {
color: var(--vp-c-brand-1);
}
</style>

View File

@@ -0,0 +1,58 @@
<script setup lang="ts">
import { inBrowser } from 'vitepress'
import { computed, provide, watchEffect } from 'vue'
import { useData } from '../composables/data'
import { navInjectionKey, useNav } from '../composables/nav'
import VPNavBar from './VPNavBar.vue'
import VPNavScreen from './VPNavScreen.vue'
const { isScreenOpen, closeScreen, toggleScreen } = useNav()
const { frontmatter } = useData()
const hasNavbar = computed(() => {
return frontmatter.value.navbar !== false
})
provide(navInjectionKey, { closeScreen })
watchEffect(() => {
if (inBrowser) {
document.documentElement.classList.toggle('hide-nav', !hasNavbar.value)
}
})
</script>
<template>
<header v-if="hasNavbar" class="VPNav">
<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: var(--vp-layout-top-height, 0px);
/*rtl:ignore*/
left: 0;
z-index: var(--vp-z-index-nav);
width: 100%;
pointer-events: none;
transition: background-color 0.5s;
overflow-x: clip;
}
@media (min-width: 960px) {
.VPNav {
position: fixed;
}
}
</style>

View File

@@ -0,0 +1,274 @@
<script lang="ts" setup>
import { useWindowScroll } from '@vueuse/core'
import { ref, watchPostEffect } from 'vue'
import { useLayout } from '../composables/layout'
import VPNavBarAppearance from './VPNavBarAppearance.vue'
import VPNavBarExtra from './VPNavBarExtra.vue'
import VPNavBarHamburger from './VPNavBarHamburger.vue'
import VPNavBarMenu from './VPNavBarMenu.vue'
import VPNavBarSearch from './VPNavBarSearch.vue'
import VPNavBarSocialLinks from './VPNavBarSocialLinks.vue'
import VPNavBarTitle from './VPNavBarTitle.vue'
import VPNavBarTranslations from './VPNavBarTranslations.vue'
const props = defineProps<{
isScreenOpen: boolean
}>()
defineEmits<{
(e: 'toggle-screen'): void
}>()
const { y } = useWindowScroll()
const { isHome, hasSidebar } = useLayout()
const classes = ref<Record<string, boolean>>({})
watchPostEffect(() => {
classes.value = {
'has-sidebar': hasSidebar.value,
'home': isHome.value,
'top': y.value === 0,
'screen-open': props.isScreenOpen
}
})
</script>
<template>
<div class="VPNavBar" :class="classes">
<div class="wrapper">
<div class="container">
<div class="title">
<VPNavBarTitle>
<template #nav-bar-title-before><slot name="nav-bar-title-before" /></template>
<template #nav-bar-title-after><slot name="nav-bar-title-after" /></template>
</VPNavBarTitle>
</div>
<div class="content">
<div class="content-body">
<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>
</div>
<div class="divider">
<div class="divider-line" />
</div>
</div>
</template>
<style scoped>
.VPNavBar {
position: relative;
height: var(--vp-nav-height);
pointer-events: none;
white-space: nowrap;
transition: background-color 0.25s;
}
.VPNavBar.screen-open {
transition: none;
background-color: var(--vp-nav-bg-color);
border-bottom: 1px solid var(--vp-c-divider);
}
.VPNavBar:not(.home) {
background-color: var(--vp-nav-bg-color);
}
@media (min-width: 960px) {
.VPNavBar:not(.home) {
background-color: transparent;
}
.VPNavBar:not(.has-sidebar):not(.home.top) {
background-color: var(--vp-nav-bg-color);
}
}
.wrapper {
padding: 0 8px 0 24px;
}
@media (min-width: 768px) {
.wrapper {
padding: 0 32px;
}
}
@media (min-width: 960px) {
.VPNavBar.has-sidebar .wrapper {
padding: 0;
}
}
.container {
display: flex;
justify-content: space-between;
margin: 0 auto;
max-width: calc(var(--vp-layout-max-width) - 64px);
height: var(--vp-nav-height);
pointer-events: none;
}
.container > .title,
.container > .content {
pointer-events: none;
}
.container :deep(*) {
pointer-events: auto;
}
@media (min-width: 960px) {
.VPNavBar.has-sidebar .container {
max-width: 100%;
}
}
.title {
flex-shrink: 0;
height: calc(var(--vp-nav-height) - 1px);
transition: background-color 0.5s;
}
@media (min-width: 960px) {
.VPNavBar.has-sidebar .title {
position: absolute;
top: 0;
left: 0;
z-index: 2;
padding: 0 32px;
width: var(--vp-sidebar-width);
height: var(--vp-nav-height);
background-color: transparent;
}
}
@media (min-width: 1440px) {
.VPNavBar.has-sidebar .title {
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);
}
}
.content {
flex-grow: 1;
}
@media (min-width: 960px) {
.VPNavBar.has-sidebar .content {
position: relative;
z-index: 1;
padding-left: var(--vp-sidebar-width);
padding-right: 32px;
}
}
@media (min-width: 1440px) {
.VPNavBar.has-sidebar .content {
padding-left: calc((100% - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width));
padding-right: calc((100% - var(--vp-layout-max-width)) / 2 + 32px);
}
}
.content-body {
display: flex;
justify-content: flex-end;
align-items: center;
height: var(--vp-nav-height);
margin-right: -100vw;
padding-right: 100vw;
transition: background-color 0.5s;
}
@media (min-width: 960px) {
.VPNavBar:not(.home.top) .content-body {
position: relative;
background-color: var(--vp-nav-bg-color);
}
.VPNavBar:not(.has-sidebar):not(.home.top) .content-body {
background-color: transparent;
}
}
@media (max-width: 767px) {
.content-body {
column-gap: 0.5rem;
}
}
.menu + .translations::before,
.menu + .appearance::before,
.menu + .social-links::before,
.translations + .appearance::before,
.appearance + .social-links::before {
margin-right: 8px;
margin-left: 8px;
width: 1px;
height: 24px;
background-color: var(--vp-c-divider);
content: "";
}
.menu + .appearance::before,
.translations + .appearance::before {
margin-right: 16px;
}
.appearance + .social-links::before {
margin-left: 16px;
}
.social-links {
margin-right: -8px;
}
.divider {
width: 100%;
height: 1px;
}
@media (min-width: 960px) {
.VPNavBar.has-sidebar .divider {
padding-left: var(--vp-sidebar-width);
}
}
@media (min-width: 1440px) {
.VPNavBar.has-sidebar .divider {
padding-left: calc((100% - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width));
}
}
.divider-line {
width: 100%;
height: 1px;
transition: background-color 0.5s;
}
.VPNavBar:not(.home) .divider-line {
background-color: var(--vp-c-gutter);
}
@media (min-width: 960px) {
.VPNavBar:not(.home.top) .divider-line {
background-color: var(--vp-c-gutter);
}
.VPNavBar:not(.has-sidebar):not(.home.top) .divider {
background-color: var(--vp-c-gutter);
}
}
</style>

View File

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

View File

@@ -0,0 +1,108 @@
<script lang="ts" setup>
import { computed } from 'vue'
import VPFlyout from './VPFlyout.vue'
import VPMenuLink from './VPMenuLink.vue'
import VPSwitchAppearance from './VPSwitchAppearance.vue'
import VPSocialLinks from './VPSocialLinks.vue'
import { useData } from '../composables/data'
import { useLangs } from '../composables/langs'
const { site, theme } = useData()
const { localeLinks, currentLang } = useLangs({ correspondingLink: true })
const hasExtraContent = computed(
() =>
(localeLinks.value.length && currentLang.value.label) ||
site.value.appearance ||
theme.value.socialLinks
)
</script>
<template>
<VPFlyout
v-if="hasExtraContent"
class="VPNavBarExtra"
label="extra navigation"
>
<div
v-if="localeLinks.length && currentLang.label"
class="group translations"
>
<p class="trans-title">{{ currentLang.label }}</p>
<template v-for="locale in localeLinks" :key="locale.link">
<VPMenuLink :item="locale" :lang="locale.lang" :dir="locale.dir" />
</template>
</div>
<div
v-if="
site.appearance &&
site.appearance !== 'force-dark' &&
site.appearance !== 'force-auto'
"
class="group"
>
<div class="item appearance">
<p class="label">
{{ theme.darkModeSwitchLabel || 'Appearance' }}
</p>
<div class="appearance-action">
<VPSwitchAppearance />
</div>
</div>
</div>
<div v-if="theme.socialLinks" class="group">
<div class="item social-links">
<VPSocialLinks class="social-links-list" :links="theme.socialLinks" />
</div>
</div>
</VPFlyout>
</template>
<style scoped>
.VPNavBarExtra {
display: none;
margin-right: -12px;
}
@media (min-width: 768px) {
.VPNavBarExtra {
display: block;
}
}
@media (min-width: 1280px) {
.VPNavBarExtra {
display: none;
}
}
.trans-title {
padding: 0 24px 0 12px;
line-height: 32px;
font-size: 14px;
font-weight: 700;
color: var(--vp-c-text-1);
}
.item.appearance,
.item.social-links {
display: flex;
align-items: center;
padding: 0 12px;
}
.item.appearance {
min-width: 176px;
}
.appearance-action {
margin-right: -2px;
}
.social-links-list {
margin: -4px -8px;
}
</style>

View File

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

View File

@@ -0,0 +1,40 @@
<script lang="ts" setup>
import { useData } from '../composables/data'
import VPNavBarMenuLink from './VPNavBarMenuLink.vue'
import VPNavBarMenuGroup from './VPNavBarMenuGroup.vue'
const { theme } = useData()
</script>
<template>
<nav
v-if="theme.nav"
aria-labelledby="main-nav-aria-label"
class="VPNavBarMenu"
>
<span id="main-nav-aria-label" class="visually-hidden">
Main Navigation
</span>
<template v-for="item in theme.nav" :key="JSON.stringify(item)">
<VPNavBarMenuLink v-if="'link' in item" :item />
<component
v-else-if="'component' in item"
:is="item.component"
v-bind="item.props"
/>
<VPNavBarMenuGroup v-else :item />
</template>
</nav>
</template>
<style scoped>
.VPNavBarMenu {
display: none;
}
@media (min-width: 768px) {
.VPNavBarMenu {
display: flex;
}
}
</style>

View File

@@ -0,0 +1,42 @@
<script lang="ts" setup>
import type { DefaultTheme } from 'vitepress/theme'
import { computed } from 'vue'
import { useData } from '../composables/data'
import { isActive } from '../../shared'
import VPFlyout from './VPFlyout.vue'
const props = defineProps<{
item: DefaultTheme.NavItemWithChildren
}>()
const { page } = useData()
const isChildActive = (navItem: DefaultTheme.NavItem) => {
if ('component' in navItem) return false
if ('link' in navItem) {
return isActive(
page.value.relativePath,
typeof navItem.link === "function" ? navItem.link(page.value) : navItem.link,
!!props.item.activeMatch
)
}
return navItem.items.some(isChildActive)
}
const childrenActive = computed(() => isChildActive(props.item))
</script>
<template>
<VPFlyout
:class="{
VPNavBarMenuGroup: true,
active:
isActive(page.relativePath, item.activeMatch, !!item.activeMatch) ||
childrenActive
}"
:button="item.text"
:items="item.items"
/>
</template>

View File

@@ -0,0 +1,60 @@
<script lang="ts" setup>
import type { DefaultTheme } from 'vitepress/theme'
import { computed } from 'vue'
import { useData } from '../composables/data'
import { isActive } from '../../shared'
import VPLink from './VPLink.vue'
const props = defineProps<{
item: DefaultTheme.NavItemWithLink
}>()
const { page } = useData()
const href = computed(() =>
typeof props.item.link === 'function'
? props.item.link(page.value)
: props.item.link
)
</script>
<template>
<VPLink
:class="{
VPNavBarMenuLink: true,
active: isActive(
page.relativePath,
item.activeMatch || href,
!!item.activeMatch
)
}"
:href
:target="item.target"
:rel="item.rel"
:no-icon="item.noIcon"
tabindex="0"
>
<span v-html="item.text"></span>
</VPLink>
</template>
<style scoped>
.VPNavBarMenuLink {
display: flex;
align-items: center;
padding: 0 12px;
line-height: var(--vp-nav-height);
font-size: 14px;
font-weight: 500;
color: var(--vp-c-text-1);
transition: color 0.25s;
}
.VPNavBarMenuLink.active {
color: var(--vp-c-brand-1);
}
.VPNavBarMenuLink:hover {
color: var(--vp-c-brand-1);
}
</style>

View File

@@ -0,0 +1,172 @@
<script lang="ts" setup>
import '@docsearch/css'
import { onKeyStroke } from '@vueuse/core'
import type { DefaultTheme } from 'vitepress/theme'
import { defineAsyncComponent, onMounted, onUnmounted, ref } from 'vue'
import { useData } from '../composables/data'
import VPNavBarSearchButton from './VPNavBarSearchButton.vue'
const VPLocalSearchBox = __VP_LOCAL_SEARCH__
? defineAsyncComponent(() => import('./VPLocalSearchBox.vue'))
: () => null
const VPAlgoliaSearchBox = __ALGOLIA__
? defineAsyncComponent(() => import('./VPAlgoliaSearchBox.vue'))
: () => null
const { theme } = useData()
// to avoid loading the docsearch js upfront (which is more than 1/3 of the
// payload), we delay initializing it until the user has actually clicked or
// hit the hotkey to invoke it.
const loaded = ref(false)
const actuallyLoaded = ref(false)
const preconnect = () => {
const id = 'VPAlgoliaPreconnect'
const rIC = window.requestIdleCallback || setTimeout
rIC(() => {
const preconnect = document.createElement('link')
preconnect.id = id
preconnect.rel = 'preconnect'
preconnect.href = `https://${
((theme.value.search?.options as DefaultTheme.AlgoliaSearchOptions) ??
theme.value.algolia)!.appId
}-dsn.algolia.net`
preconnect.crossOrigin = ''
document.head.appendChild(preconnect)
})
}
onMounted(() => {
if (!__ALGOLIA__) {
return
}
preconnect()
const handleSearchHotKey = (event: KeyboardEvent) => {
if (
(event.key?.toLowerCase() === 'k' && (event.metaKey || event.ctrlKey)) ||
(!isEditingContent(event) && event.key === '/')
) {
event.preventDefault()
load()
remove()
}
}
const remove = () => {
window.removeEventListener('keydown', handleSearchHotKey)
}
window.addEventListener('keydown', handleSearchHotKey)
onUnmounted(remove)
})
function load() {
if (!loaded.value) {
loaded.value = true
setTimeout(poll, 16)
}
}
function poll() {
// programmatically open the search box after initialize
const e = new Event('keydown') as any
e.key = 'k'
e.metaKey = true
window.dispatchEvent(e)
setTimeout(() => {
if (!document.querySelector('.DocSearch-Modal')) {
poll()
}
}, 16)
}
function isEditingContent(event: KeyboardEvent): boolean {
const element = event.target as HTMLElement
const tagName = element.tagName
return (
element.isContentEditable ||
tagName === 'INPUT' ||
tagName === 'SELECT' ||
tagName === 'TEXTAREA'
)
}
// Local search
const showSearch = ref(false)
if (__VP_LOCAL_SEARCH__) {
onKeyStroke('k', (event) => {
if (event.ctrlKey || event.metaKey) {
event.preventDefault()
showSearch.value = true
}
})
onKeyStroke('/', (event) => {
if (!isEditingContent(event)) {
event.preventDefault()
showSearch.value = true
}
})
}
const provider = __ALGOLIA__ ? 'algolia' : __VP_LOCAL_SEARCH__ ? 'local' : ''
</script>
<template>
<div class="VPNavBarSearch">
<template v-if="provider === 'local'">
<VPLocalSearchBox
v-if="showSearch"
@close="showSearch = false"
/>
<div id="local-search">
<VPNavBarSearchButton @click="showSearch = true" />
</div>
</template>
<template v-else-if="provider === 'algolia'">
<VPAlgoliaSearchBox
v-if="loaded"
:algolia="theme.search?.options ?? theme.algolia"
@vue:beforeMount="actuallyLoaded = true"
/>
<div v-if="!actuallyLoaded" id="docsearch">
<VPNavBarSearchButton @click="load" />
</div>
</template>
</div>
</template>
<style>
.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;
}
}
</style>

View File

@@ -0,0 +1,147 @@
<script lang="ts" setup>
import type { ButtonTranslations } from '../../../../types/local-search'
import { createSearchTranslate } from '../support/translation'
// button translations
const defaultTranslations: { button: ButtonTranslations } = {
button: {
buttonText: 'Search',
buttonAriaLabel: 'Search'
}
}
const translate = createSearchTranslate(defaultTranslations)
</script>
<template>
<button
type="button"
:aria-label="translate('button.buttonAriaLabel')"
aria-keyshortcuts="/ control+k meta+k"
class="DocSearch DocSearch-Button"
>
<span class="DocSearch-Button-Container">
<span class="vpi-search DocSearch-Search-Icon"></span>
<span class="DocSearch-Button-Placeholder">{{ translate('button.buttonText') }}</span>
</span>
<span class="DocSearch-Button-Keys">
<kbd class="DocSearch-Button-Key"></kbd>
<kbd class="DocSearch-Button-Key"></kbd>
</span>
</button>
</template>
<style>
[class*='DocSearch'] {
--docsearch-actions-height: auto;
--docsearch-actions-width: auto;
--docsearch-background-color: var(--vp-c-bg-soft);
--docsearch-container-background: var(--vp-backdrop-bg-color);
--docsearch-focus-color: var(--vp-c-brand-1);
--docsearch-footer-background: var(--vp-c-bg);
--docsearch-highlight-color: var(--vp-c-brand-1);
--docsearch-hit-background: var(--vp-c-default-soft);
--docsearch-hit-color: var(--vp-c-text-1);
--docsearch-hit-highlight-color: var(--vp-c-brand-soft);
--docsearch-icon-color: var(--vp-c-text-2);
--docsearch-key-background: transparent;
--docsearch-key-color: var(--vp-c-text-2);
--docsearch-modal-background: var(--vp-c-bg-soft);
--docsearch-muted-color: var(--vp-c-text-2);
--docsearch-primary-color: var(--vp-c-brand-1);
--docsearch-searchbox-focus-background: transparent;
--docsearch-secondary-text-color: var(--vp-c-text-2);
--docsearch-soft-primary-color: var(--vp-c-brand-soft);
--docsearch-subtle-color: var(--vp-c-divider);
--docsearch-success-color: var(--vp-c-brand-soft);
--docsearch-text-color: var(--vp-c-text-1);
}
.dark [class*='DocSearch'] {
--docsearch-modal-shadow: none;
}
.DocSearch-Clear {
padding: 0 8px;
}
.DocSearch-Commands-Key {
padding: 4px;
border: 1px solid var(--docsearch-subtle-color);
border-radius: 4px;
}
.DocSearch-Hit a:focus-visible {
outline: 2px solid var(--docsearch-focus-color);
}
.DocSearch-Logo [class^='cls-'] {
fill: currentColor;
}
.DocSearch-SearchBar + .DocSearch-Footer {
border-top-color: transparent;
}
.DocSearch-Title {
font-size: revert;
line-height: revert;
}
.DocSearch-Button {
--docsearch-muted-color: var(--docsearch-text-color);
--docsearch-searchbox-background: transparent;
width: auto;
padding: 2px 12px;
border: none;
border-radius: 8px;
}
.DocSearch-Search-Icon {
color: inherit !important;
width: 20px;
height: 20px;
}
@media (min-width: 768px) {
.DocSearch-Button {
--docsearch-muted-color: var(--docsearch-secondary-text-color);
--docsearch-searchbox-background: var(--vp-c-bg-alt);
}
.DocSearch-Search-Icon {
width: 15px;
height: 15px;
}
.DocSearch-Button-Placeholder {
font-size: 13px;
}
}
.DocSearch-Button-Keys {
min-width: auto;
margin: 0;
padding: 4px 6px;
background-color: var(--docsearch-key-background);
border: 1px solid var(--docsearch-subtle-color);
border-radius: 4px;
font-size: 12px;
line-height: 1;
color: var(--docsearch-key-color);
}
.DocSearch-Button-Keys > * {
display: none;
}
.DocSearch-Button-Keys:after {
/*rtl:ignore*/
direction: ltr;
content: 'Ctrl K';
}
.mac .DocSearch-Button-Keys:after {
content: '\2318 K';
}
</style>

View File

@@ -0,0 +1,27 @@
<script lang="ts" setup>
import { useData } from '../composables/data'
import VPSocialLinks from './VPSocialLinks.vue'
const { theme } = useData()
</script>
<template>
<VPSocialLinks
v-if="theme.socialLinks"
class="VPNavBarSocialLinks"
:links="theme.socialLinks"
/>
</template>
<style scoped>
.VPNavBarSocialLinks {
display: none;
}
@media (min-width: 1280px) {
.VPNavBarSocialLinks {
display: flex;
align-items: center;
}
}
</style>

View File

@@ -0,0 +1,76 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useData } from '../composables/data'
import { useLangs } from '../composables/langs'
import { useLayout } from '../composables/layout'
import { normalizeLink } from '../support/utils'
import VPImage from './VPImage.vue'
const { site, theme } = useData()
const { hasSidebar } = useLayout()
const { currentLang } = useLangs()
const link = computed(() =>
typeof theme.value.logoLink === 'string'
? theme.value.logoLink
: theme.value.logoLink?.link
)
const rel = computed(() =>
typeof theme.value.logoLink === 'string'
? undefined
: theme.value.logoLink?.rel
)
const target = computed(() =>
typeof theme.value.logoLink === 'string'
? undefined
: theme.value.logoLink?.target
)
</script>
<template>
<div class="VPNavBarTitle" :class="{ 'has-sidebar': hasSidebar }">
<a
class="title"
:href="link ?? normalizeLink(currentLang.link)"
:rel
:target
>
<slot name="nav-bar-title-before" />
<VPImage v-if="theme.logo" class="logo" :image="theme.logo" />
<span v-if="theme.siteTitle" v-html="theme.siteTitle"></span>
<span v-else-if="theme.siteTitle === undefined">{{ site.title }}</span>
<slot name="nav-bar-title-after" />
</a>
</div>
</template>
<style scoped>
.title {
display: flex;
align-items: center;
border-bottom: 1px solid transparent;
width: 100%;
height: var(--vp-nav-height);
font-size: 16px;
font-weight: 600;
color: var(--vp-c-text-1);
transition: opacity 0.25s;
}
@media (min-width: 960px) {
.title {
flex-shrink: 0;
}
.VPNavBarTitle.has-sidebar .title {
border-bottom-color: var(--vp-c-divider);
}
}
:deep(.logo) {
margin-right: 8px;
height: var(--vp-nav-logo-height);
}
</style>

View File

@@ -0,0 +1,47 @@
<script lang="ts" setup>
import VPFlyout from './VPFlyout.vue'
import VPMenuLink from './VPMenuLink.vue'
import { useData } from '../composables/data'
import { useLangs } from '../composables/langs'
const { theme } = useData()
const { localeLinks, currentLang } = useLangs({ correspondingLink: true })
</script>
<template>
<VPFlyout
v-if="localeLinks.length && currentLang.label"
class="VPNavBarTranslations"
icon="vpi-languages"
:label="theme.langMenuLabel || 'Change language'"
>
<div class="items">
<p class="title">{{ currentLang.label }}</p>
<template v-for="locale in localeLinks" :key="locale.link">
<VPMenuLink :item="locale" :lang="locale.lang" :dir="locale.dir" />
</template>
</div>
</VPFlyout>
</template>
<style scoped>
.VPNavBarTranslations {
display: none;
}
@media (min-width: 1280px) {
.VPNavBarTranslations {
display: flex;
align-items: center;
}
}
.title {
padding: 0 24px 0 12px;
line-height: 32px;
font-size: 14px;
font-weight: 700;
color: var(--vp-c-text-1);
}
</style>

View File

@@ -0,0 +1,99 @@
<script setup lang="ts">
import { useScrollLock } from '@vueuse/core'
import { inBrowser } from 'vitepress'
import { ref } from 'vue'
import VPNavScreenAppearance from './VPNavScreenAppearance.vue'
import VPNavScreenMenu from './VPNavScreenMenu.vue'
import VPNavScreenSocialLinks from './VPNavScreenSocialLinks.vue'
import VPNavScreenTranslations from './VPNavScreenTranslations.vue'
defineProps<{
open: boolean
}>()
const screen = ref<HTMLElement | null>(null)
const isLocked = useScrollLock(inBrowser ? document.body : null)
</script>
<template>
<transition
name="fade"
@enter="isLocked = true"
@after-leave="isLocked = false"
>
<div v-if="open" class="VPNavScreen" ref="screen" id="VPNavScreen">
<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: calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px));
/*rtl:ignore*/
right: 0;
bottom: 0;
/*rtl:ignore*/
left: 0;
padding: 0 32px;
width: 100%;
background-color: var(--vp-nav-screen-bg-color);
overflow-y: auto;
transition: background-color 0.25s;
pointer-events: auto;
}
.VPNavScreen.fade-enter-active,
.VPNavScreen.fade-leave-active {
transition: opacity 0.25s;
}
.VPNavScreen.fade-enter-active .container,
.VPNavScreen.fade-leave-active .container {
transition: transform 0.25s ease;
}
.VPNavScreen.fade-enter-from,
.VPNavScreen.fade-leave-to {
opacity: 0;
}
.VPNavScreen.fade-enter-from .container,
.VPNavScreen.fade-leave-to .container {
transform: translateY(-8px);
}
@media (min-width: 768px) {
.VPNavScreen {
display: none;
}
}
.container {
margin: 0 auto;
padding: 24px 0 96px;
max-width: 288px;
}
.menu + .translations,
.menu + .appearance,
.translations + .appearance {
margin-top: 24px;
}
.menu + .social-links {
margin-top: 16px;
}
.appearance + .social-links {
margin-top: 16px;
}
</style>

View File

@@ -0,0 +1,40 @@
<script lang="ts" setup>
import { useData } from '../composables/data'
import VPSwitchAppearance from './VPSwitchAppearance.vue'
const { site, theme } = useData()
</script>
<template>
<div
v-if="
site.appearance &&
site.appearance !== 'force-dark' &&
site.appearance !== 'force-auto'
"
class="VPNavScreenAppearance"
>
<p class="text">
{{ theme.darkModeSwitchLabel || 'Appearance' }}
</p>
<VPSwitchAppearance />
</div>
</template>
<style scoped>
.VPNavScreenAppearance {
display: flex;
justify-content: space-between;
align-items: center;
border-radius: 8px;
padding: 12px 14px 12px 16px;
background-color: var(--vp-c-bg-soft);
}
.text {
line-height: 24px;
font-size: 12px;
font-weight: 500;
color: var(--vp-c-text-2);
}
</style>

View File

@@ -0,0 +1,26 @@
<script lang="ts" setup>
import { useData } from '../composables/data'
import VPNavScreenMenuLink from './VPNavScreenMenuLink.vue'
import VPNavScreenMenuGroup from './VPNavScreenMenuGroup.vue'
const { theme } = useData()
</script>
<template>
<nav v-if="theme.nav" class="VPNavScreenMenu">
<template v-for="item in theme.nav" :key="JSON.stringify(item)">
<VPNavScreenMenuLink v-if="'link' in item" :item />
<component
v-else-if="'component' in item"
:is="item.component"
v-bind="item.props"
screen-menu
/>
<VPNavScreenMenuGroup
v-else
:text="item.text || ''"
:items="item.items"
/>
</template>
</nav>
</template>

View File

@@ -0,0 +1,112 @@
<script lang="ts" setup>
import { computed, ref } from 'vue'
import VPNavScreenMenuGroupLink from './VPNavScreenMenuGroupLink.vue'
import VPNavScreenMenuGroupSection from './VPNavScreenMenuGroupSection.vue'
const props = defineProps<{
text: string
items: any[]
}>()
const isOpen = ref(false)
const groupId = computed(
() => `NavScreenGroup-${props.text.replace(' ', '-').toLowerCase()}`
)
function toggle() {
isOpen.value = !isOpen.value
}
</script>
<template>
<div class="VPNavScreenMenuGroup" :class="{ open: isOpen }">
<button
class="button"
:aria-controls="groupId"
:aria-expanded="isOpen"
@click="toggle"
>
<span class="button-text" v-html="text"></span>
<span class="vpi-plus button-icon" />
</button>
<div :id="groupId" class="items">
<template v-for="item in items" :key="JSON.stringify(item)">
<div v-if="'link' in item" class="item">
<VPNavScreenMenuGroupLink :item />
</div>
<div v-else-if="'component' in item" class="item">
<component :is="item.component" v-bind="item.props" screen-menu />
</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);
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-1);
}
.VPNavScreenMenuGroup.open .button-icon {
/*rtl:ignore*/
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-1);
}
.button-icon {
transition: transform 0.25s;
}
.group:first-child {
padding-top: 0px;
}
.group + .group,
.group + .item {
padding-top: 4px;
}
</style>

View File

@@ -0,0 +1,50 @@
<script lang="ts" setup>
import type { DefaultTheme } from 'vitepress/theme'
import { computed, inject } from 'vue'
import { useData } from '../composables/data'
import { navInjectionKey } from '../composables/nav'
import VPLink from './VPLink.vue'
const props = defineProps<{
item: DefaultTheme.NavItemWithLink
}>()
const { page } = useData()
const href = computed(() =>
typeof props.item.link === 'function'
? props.item.link(page.value)
: props.item.link
)
const { closeScreen } = inject(navInjectionKey)!
</script>
<template>
<VPLink
class="VPNavScreenMenuGroupLink"
:href
:target="item.target"
:rel="item.rel"
:no-icon="item.noIcon"
@click="closeScreen"
>
<span v-html="item.text"></span>
</VPLink>
</template>
<style scoped>
.VPNavScreenMenuGroupLink {
display: block;
margin-left: 12px;
line-height: 32px;
font-size: 14px;
font-weight: 400;
color: var(--vp-c-text-1);
transition: color 0.25s;
}
.VPNavScreenMenuGroupLink:hover {
color: var(--vp-c-brand-1);
}
</style>

View File

@@ -0,0 +1,30 @@
<script lang="ts" setup>
import type { DefaultTheme } from 'vitepress/theme'
import VPNavScreenMenuGroupLink from './VPNavScreenMenuGroupLink.vue'
defineProps<{
text?: string
items: DefaultTheme.NavItemWithLink[]
}>()
</script>
<template>
<div class="VPNavScreenMenuGroupSection">
<p v-if="text" class="title">{{ text }}</p>
<VPNavScreenMenuGroupLink v-for="item in items" :key="item.text" :item />
</div>
</template>
<style scoped>
.VPNavScreenMenuGroupSection {
display: block;
}
.title {
line-height: 32px;
font-size: 13px;
font-weight: 700;
color: var(--vp-c-text-2);
transition: color 0.25s;
}
</style>

View File

@@ -0,0 +1,53 @@
<script lang="ts" setup>
import type { DefaultTheme } from 'vitepress/theme'
import { computed, inject } from 'vue'
import { useData } from '../composables/data'
import { navInjectionKey } from '../composables/nav'
import VPLink from './VPLink.vue'
const props = defineProps<{
item: DefaultTheme.NavItemWithLink
}>()
const { page } = useData()
const href = computed(() =>
typeof props.item.link === 'function'
? props.item.link(page.value)
: props.item.link
)
const { closeScreen } = inject(navInjectionKey)!
</script>
<template>
<VPLink
class="VPNavScreenMenuLink"
:href
:target="item.target"
:rel="item.rel"
:no-icon="item.noIcon"
@click="closeScreen"
>
<span v-html="item.text"></span>
</VPLink>
</template>
<style scoped>
.VPNavScreenMenuLink {
display: block;
border-bottom: 1px solid var(--vp-c-divider);
padding: 12px 0 11px;
line-height: 24px;
font-size: 14px;
font-weight: 500;
color: var(--vp-c-text-1);
transition:
border-color 0.25s,
color 0.25s;
}
.VPNavScreenMenuLink:hover {
color: var(--vp-c-brand-1);
}
</style>

View File

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

View File

@@ -0,0 +1,80 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useLangs } from '../composables/langs'
import VPLink from './VPLink.vue'
const { localeLinks, currentLang } = useLangs({ correspondingLink: true })
const isOpen = ref(false)
function toggle() {
isOpen.value = !isOpen.value
}
</script>
<template>
<div
v-if="localeLinks.length && currentLang.label"
class="VPNavScreenTranslations"
:class="{ open: isOpen }"
>
<button class="title" @click="toggle">
<span class="vpi-languages icon lang" />
{{ currentLang.label }}
<span class="vpi-chevron-down icon chevron" />
</button>
<ul class="list">
<li v-for="locale in localeLinks" :key="locale.link" class="item">
<VPLink
class="link"
:href="locale.link"
:lang="locale.lang"
:dir="locale.dir"
>
{{ locale.text }}
</VPLink>
</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 {
font-size: 16px;
}
.icon.lang {
margin-right: 8px;
}
.icon.chevron {
margin-left: 4px;
}
.list {
padding: 4px 0 0 24px;
}
.link {
line-height: 32px;
font-size: 13px;
color: var(--vp-c-text-1);
}
</style>

View File

@@ -0,0 +1,7 @@
<template>
<div class="VPPage">
<slot name="page-top" />
<Content />
<slot name="page-bottom" />
</div>
</template>

View File

@@ -0,0 +1,136 @@
<script lang="ts" setup>
import { useScrollLock } from '@vueuse/core'
import { inBrowser } from 'vitepress'
import { ref, watch } from 'vue'
import { useLayout } from '../composables/layout'
import VPSidebarGroup from './VPSidebarGroup.vue'
const { sidebarGroups, hasSidebar } = useLayout()
const props = defineProps<{
open: boolean
}>()
// a11y: focus Nav element when menu has opened
const navEl = ref<HTMLElement | null>(null)
const isLocked = useScrollLock(inBrowser ? document.body : null)
watch(
[props, navEl],
() => {
if (props.open) {
isLocked.value = true
navEl.value?.focus()
} else isLocked.value = false
},
{ immediate: true, flush: 'post' }
)
const key = ref(0)
watch(
sidebarGroups,
() => {
key.value += 1
},
{ deep: true }
)
</script>
<template>
<aside
v-if="hasSidebar"
class="VPSidebar"
:class="{ open }"
ref="navEl"
@click.stop
>
<div class="curtain" />
<nav
class="nav"
id="VPSidebarNav"
aria-labelledby="sidebar-aria-label"
tabindex="-1"
>
<span class="visually-hidden" id="sidebar-aria-label">
Sidebar Navigation
</span>
<slot name="sidebar-nav-before" />
<VPSidebarGroup :items="sidebarGroups" :key />
<slot name="sidebar-nav-after" />
</nav>
</aside>
</template>
<style scoped>
.VPSidebar {
position: fixed;
top: var(--vp-layout-top-height, 0px);
bottom: 0;
left: 0;
z-index: var(--vp-z-index-sidebar);
padding: 32px 32px 96px;
width: calc(100vw - 64px);
max-width: 320px;
background-color: var(--vp-sidebar-bg-color);
opacity: 0;
box-shadow: var(--vp-c-shadow-3);
overflow-x: hidden;
overflow-y: auto;
transform: translateX(-100%);
transition: opacity 0.5s, transform 0.25s ease;
overscroll-behavior: contain;
}
.VPSidebar.open {
opacity: 1;
visibility: visible;
transform: translateX(0);
transition: opacity 0.25s,
transform 0.5s cubic-bezier(0.19, 1, 0.22, 1);
}
.dark .VPSidebar {
box-shadow: var(--vp-shadow-1);
}
@media (min-width: 960px) {
.VPSidebar {
padding-top: var(--vp-nav-height);
width: var(--vp-sidebar-width);
max-width: 100%;
background-color: var(--vp-sidebar-bg-color);
opacity: 1;
visibility: visible;
box-shadow: none;
transform: translateX(0);
}
}
@media (min-width: 1440px) {
.VPSidebar {
padding-left: max(32px, calc((100% - (var(--vp-layout-max-width) - 64px)) / 2));
width: calc((100% - (var(--vp-layout-max-width) - 64px)) / 2 + var(--vp-sidebar-width) - 32px);
}
}
@media (min-width: 960px) {
.curtain {
position: sticky;
top: calc(var(--vp-nav-height) * -1);
left: 0;
z-index: 1;
margin-top: calc(var(--vp-nav-height) * -1);
margin-right: -32px;
margin-left: -32px;
height: var(--vp-nav-height);
background-color: var(--vp-sidebar-bg-color);
}
}
.nav {
outline: 0;
}
</style>

View File

@@ -0,0 +1,56 @@
<script setup lang="ts">
import type { DefaultTheme } from 'vitepress/theme'
import { onBeforeUnmount, onMounted, ref } from 'vue'
import VPSidebarItem from './VPSidebarItem.vue'
defineProps<{
items: DefaultTheme.SidebarItem[]
}>()
const disableTransition = ref(true)
let timer: ReturnType<typeof setTimeout> | null = null
onMounted(() => {
timer = setTimeout(() => {
timer = null
disableTransition.value = false
}, 300)
})
onBeforeUnmount(() => {
if (timer != null) {
clearTimeout(timer)
timer = null
}
})
</script>
<template>
<div
v-for="item in items"
:key="item.text"
class="group"
:class="{ 'no-transition': disableTransition }"
>
<VPSidebarItem :item :depth="0" />
</div>
</template>
<style scoped>
.no-transition :deep(.caret-icon) {
transition: none;
}
.group + .group {
border-top: 1px solid var(--vp-c-divider);
padding-top: 10px;
}
@media (min-width: 960px) {
.group {
padding-top: 10px;
width: calc(var(--vp-sidebar-width) - 64px);
}
}
</style>

View File

@@ -0,0 +1,251 @@
<script setup lang="ts">
import type { DefaultTheme } from 'vitepress/theme'
import { computed } from 'vue'
import { useSidebarItemControl } from '../composables/sidebar'
import VPLink from './VPLink.vue'
const props = defineProps<{
item: DefaultTheme.SidebarItem
depth: number
}>()
const {
collapsed,
collapsible,
isLink,
isActiveLink,
hasActiveLink,
hasChildren,
toggle
} = useSidebarItemControl(computed(() => props.item))
const sectionTag = computed(() => (hasChildren.value ? 'section' : `div`))
const linkTag = computed(() => (isLink.value ? 'a' : 'div'))
const textTag = computed(() => {
return !hasChildren.value
? 'p'
: props.depth + 2 === 7
? 'p'
: `h${props.depth + 2}`
})
const itemRole = computed(() => (isLink.value ? undefined : 'button'))
const classes = computed(() => [
[`level-${props.depth}`],
{ collapsible: collapsible.value },
{ collapsed: collapsed.value },
{ 'is-link': isLink.value },
{ 'is-active': isActiveLink.value },
{ 'has-active': hasActiveLink.value }
])
function onItemInteraction(e: MouseEvent | Event) {
if ('key' in e && e.key !== 'Enter') {
return
}
!props.item.link && toggle()
}
function onCaretClick() {
props.item.link && toggle()
}
</script>
<template>
<component :is="sectionTag" class="VPSidebarItem" :class="classes">
<div
v-if="item.text"
class="item"
:role="itemRole"
v-on="
item.items
? { click: onItemInteraction, keydown: onItemInteraction }
: {}
"
:tabindex="item.items && 0"
>
<div class="indicator" />
<VPLink
v-if="item.link"
:tag="linkTag"
class="link"
:href="item.link"
:rel="item.rel"
:target="item.target"
>
<component :is="textTag" class="text" v-html="item.text" />
</VPLink>
<component v-else :is="textTag" class="text" v-html="item.text" />
<div
v-if="item.collapsed != null && item.items && item.items.length"
class="caret"
role="button"
aria-label="toggle section"
@click="onCaretClick"
@keydown.enter="onCaretClick"
tabindex="0"
>
<span class="vpi-chevron-right caret-icon" />
</div>
</div>
<div v-if="item.items && item.items.length" class="items">
<template v-if="depth < 5">
<VPSidebarItem
v-for="i in item.items"
:key="i.text"
:item="i"
:depth="depth + 1"
/>
</template>
</div>
</component>
</template>
<style scoped>
.VPSidebarItem.level-0 {
padding-bottom: 24px;
}
.VPSidebarItem.collapsed.level-0 {
padding-bottom: 10px;
}
.item {
position: relative;
display: flex;
width: 100%;
}
.VPSidebarItem.collapsible > .item {
cursor: pointer;
}
.indicator {
position: absolute;
top: 6px;
bottom: 6px;
left: -17px;
width: 2px;
border-radius: 2px;
transition: background-color 0.25s;
}
.VPSidebarItem.level-2.is-active > .item > .indicator,
.VPSidebarItem.level-3.is-active > .item > .indicator,
.VPSidebarItem.level-4.is-active > .item > .indicator,
.VPSidebarItem.level-5.is-active > .item > .indicator {
background-color: var(--vp-c-brand-1);
}
.link {
display: flex;
align-items: center;
flex-grow: 1;
}
.text {
flex-grow: 1;
padding: 4px 0;
line-height: 24px;
font-size: 14px;
transition: color 0.25s;
}
.VPSidebarItem.level-0 .text {
font-weight: 700;
color: var(--vp-c-text-1);
}
.VPSidebarItem.level-1 .text,
.VPSidebarItem.level-2 .text,
.VPSidebarItem.level-3 .text,
.VPSidebarItem.level-4 .text,
.VPSidebarItem.level-5 .text {
font-weight: 500;
color: var(--vp-c-text-2);
}
.VPSidebarItem.level-0.is-link > .item > .link:hover .text,
.VPSidebarItem.level-1.is-link > .item > .link:hover .text,
.VPSidebarItem.level-2.is-link > .item > .link:hover .text,
.VPSidebarItem.level-3.is-link > .item > .link:hover .text,
.VPSidebarItem.level-4.is-link > .item > .link:hover .text,
.VPSidebarItem.level-5.is-link > .item > .link:hover .text {
color: var(--vp-c-brand-1);
}
.VPSidebarItem.level-0.has-active > .item > .text,
.VPSidebarItem.level-1.has-active > .item > .text,
.VPSidebarItem.level-2.has-active > .item > .text,
.VPSidebarItem.level-3.has-active > .item > .text,
.VPSidebarItem.level-4.has-active > .item > .text,
.VPSidebarItem.level-5.has-active > .item > .text,
.VPSidebarItem.level-0.has-active > .item > .link > .text,
.VPSidebarItem.level-1.has-active > .item > .link > .text,
.VPSidebarItem.level-2.has-active > .item > .link > .text,
.VPSidebarItem.level-3.has-active > .item > .link > .text,
.VPSidebarItem.level-4.has-active > .item > .link > .text,
.VPSidebarItem.level-5.has-active > .item > .link > .text {
color: var(--vp-c-text-1);
}
.VPSidebarItem.level-0.is-active > .item .link > .text,
.VPSidebarItem.level-1.is-active > .item .link > .text,
.VPSidebarItem.level-2.is-active > .item .link > .text,
.VPSidebarItem.level-3.is-active > .item .link > .text,
.VPSidebarItem.level-4.is-active > .item .link > .text,
.VPSidebarItem.level-5.is-active > .item .link > .text {
color: var(--vp-c-brand-1);
}
.caret {
display: flex;
justify-content: center;
align-items: center;
margin-right: -7px;
width: 32px;
height: 32px;
color: var(--vp-c-text-3);
cursor: pointer;
transition: color 0.25s;
flex-shrink: 0;
}
.item:hover .caret {
color: var(--vp-c-text-2);
}
.item:hover .caret:hover {
color: var(--vp-c-text-1);
}
.caret-icon {
font-size: 18px;
/*rtl:ignore*/
transform: rotate(90deg);
transition: transform 0.25s;
}
.VPSidebarItem.collapsed .caret-icon {
transform: rotate(0)/*rtl:rotate(180deg)*/;
}
.VPSidebarItem.level-1 .items,
.VPSidebarItem.level-2 .items,
.VPSidebarItem.level-3 .items,
.VPSidebarItem.level-4 .items,
.VPSidebarItem.level-5 .items {
border-left: 1px solid var(--vp-c-divider);
padding-left: 16px;
}
.VPSidebarItem.collapsed .items {
display: none;
}
</style>

View File

@@ -0,0 +1,49 @@
<script lang="ts" setup>
import { ref, watch } from 'vue'
import { useRoute } from 'vitepress'
import { useData } from '../composables/data'
const { theme } = useData()
const route = useRoute()
const backToTop = ref()
watch(() => route.path, () => backToTop.value.focus())
</script>
<template>
<span ref="backToTop" tabindex="-1" />
<a href="#VPContent" class="VPSkipLink visually-hidden">
{{ theme.skipToContentLabel || 'Skip to content' }}
</a>
</template>
<style scoped>
.VPSkipLink {
position: fixed;
top: 8px;
left: 8px;
padding: 8px 16px;
z-index: 999;
border-radius: 8px;
font-size: 12px;
font-weight: bold;
text-decoration: none;
color: var(--vp-c-brand-1);
box-shadow: var(--vp-shadow-3);
background-color: var(--vp-c-bg);
}
.VPSkipLink:focus {
height: auto;
width: auto;
clip: auto;
clip-path: none;
}
@media (min-width: 1280px) {
.VPSkipLink {
top: 14px;
left: 16px;
}
}
</style>

View File

@@ -0,0 +1,76 @@
<script lang="ts" setup>
import type { DefaultTheme } from 'vitepress/theme'
import { computed, nextTick, onMounted, ref, useSSRContext } from 'vue'
import type { SSGContext } from '../../shared'
const props = defineProps<{
icon: DefaultTheme.SocialLinkIcon
link: string
ariaLabel?: string
me: boolean
}>()
const el = ref<HTMLAnchorElement>()
onMounted(async () => {
await nextTick()
const span = el.value?.children[0]
if (
span instanceof HTMLElement &&
span.className.startsWith('vpi-social-') &&
(getComputedStyle(span).maskImage ||
getComputedStyle(span).webkitMaskImage) === 'none'
) {
span.style.setProperty(
'--icon',
`url('https://api.iconify.design/simple-icons/${props.icon}.svg')`
)
}
})
const svg = computed(() => {
if (typeof props.icon === 'object') return props.icon.svg
return `<span class="vpi-social-${props.icon}"></span>`
})
if (import.meta.env.SSR) {
typeof props.icon === 'string' &&
useSSRContext<SSGContext>()?.vpSocialIcons.add(props.icon)
}
</script>
<template>
<a
ref="el"
class="VPSocialLink no-icon"
:href="link"
:aria-label="ariaLabel ?? (typeof icon === 'string' ? icon : '')"
target="_blank"
:rel="me ? 'me noopener' : '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),
.VPSocialLink > :deep([class^="vpi-social-"]) {
width: 20px;
height: 20px;
fill: currentColor;
}
</style>

View File

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

View File

@@ -0,0 +1,48 @@
<script setup lang="ts">
import type { GridSize } from '../composables/sponsor-grid'
import type { Sponsor } from './VPSponsorsGrid.vue'
import { computed } from 'vue'
import VPSponsorsGrid from './VPSponsorsGrid.vue'
export interface Sponsors {
tier?: string
size?: GridSize
items: Sponsor[]
}
interface Props {
mode?: 'normal' | 'aside'
tier?: string
size?: GridSize
data: Sponsors[] | Sponsor[]
}
const props = withDefaults(defineProps<Props>(), {
mode: 'normal'
})
const sponsors = computed(() => {
const isSponsors = props.data.some((s) => {
return 'items' in s
})
if (isSponsors) {
return props.data as Sponsors[]
}
return [
{ tier: props.tier, size: props.size, items: props.data as Sponsor[] }
]
})
</script>
<template>
<div class="VPSponsors vp-sponsor" :class="[mode]">
<section
v-for="(sponsor, index) in sponsors"
:key="index"
class="vp-sponsor-section"
>
<h3 v-if="sponsor.tier" class="vp-sponsor-tier">{{ sponsor.tier }}</h3>
<VPSponsorsGrid :size="sponsor.size" :data="sponsor.items" />
</section>
</div>
</template>

View File

@@ -0,0 +1,47 @@
<script setup lang="ts">
import type { GridSize } from '../composables/sponsor-grid'
import { ref } from 'vue'
import { useSponsorsGrid } from '../composables/sponsor-grid'
export interface Sponsor {
name: string
img: string
url: string
}
interface Props {
size?: GridSize
data: Sponsor[]
}
const props = withDefaults(defineProps<Props>(), {
size: 'medium'
})
const el = ref(null)
useSponsorsGrid({ el, size: props.size })
</script>
<template>
<div class="VPSponsorsGrid vp-sponsor-grid" :class="[size]" ref="el">
<div
v-for="sponsor in data"
:key="sponsor.name"
class="vp-sponsor-grid-item"
>
<a
class="vp-sponsor-grid-link"
:href="sponsor.url"
target="_blank"
rel="sponsored noopener"
>
<article class="vp-sponsor-grid-box">
<img
class="vp-sponsor-grid-image"
:src="sponsor.img"
:alt="sponsor.name"
/>
</article>
</a>
</div>
</div>
</template>

View File

@@ -0,0 +1,63 @@
<template>
<button class="VPSwitch" type="button" role="switch">
<span class="check">
<span class="icon" v-if="$slots.default">
<slot />
</span>
</span>
</button>
</template>
<style scoped>
.VPSwitch {
position: relative;
border-radius: 11px;
display: block;
width: 40px;
height: 22px;
flex-shrink: 0;
border: 1px solid var(--vp-input-border-color);
background-color: var(--vp-input-switch-bg-color);
transition: border-color 0.25s !important;
}
.VPSwitch:hover {
border-color: var(--vp-c-brand-1);
}
.check {
position: absolute;
top: 1px;
/*rtl:ignore*/
left: 1px;
width: 18px;
height: 18px;
border-radius: 50%;
background-color: var(--vp-c-neutral-inverse);
box-shadow: var(--vp-shadow-1);
transition: transform 0.25s !important;
}
.icon {
position: relative;
display: block;
width: 18px;
height: 18px;
border-radius: 50%;
overflow: hidden;
}
.icon :deep([class^='vpi-']) {
position: absolute;
top: 3px;
left: 3px;
width: 12px;
height: 12px;
color: var(--vp-c-text-2);
}
.dark .icon :deep([class^='vpi-']) {
color: var(--vp-c-text-1);
transition: opacity 0.25s !important;
}
</style>

View File

@@ -0,0 +1,54 @@
<script lang="ts" setup>
import { inject, ref, watchPostEffect } from 'vue'
import { useData } from '../composables/data'
import VPSwitch from './VPSwitch.vue'
const { isDark, theme } = useData()
const toggleAppearance = inject('toggle-appearance', () => {
isDark.value = !isDark.value
})
const switchTitle = ref('')
watchPostEffect(() => {
switchTitle.value = isDark.value
? theme.value.lightModeSwitchTitle || 'Switch to light theme'
: theme.value.darkModeSwitchTitle || 'Switch to dark theme'
})
</script>
<template>
<VPSwitch
:title="switchTitle"
class="VPSwitchAppearance"
:aria-checked="isDark"
@click="toggleAppearance"
>
<span class="vpi-sun sun" />
<span class="vpi-moon moon" />
</VPSwitch>
</template>
<style scoped>
.sun {
opacity: 1;
}
.moon {
opacity: 0;
}
.dark .sun {
opacity: 0;
}
.dark .moon {
opacity: 1;
}
.dark .VPSwitchAppearance :deep(.check) {
/*rtl:ignore*/
transform: translateX(18px);
}
</style>

View File

@@ -0,0 +1,66 @@
<script setup lang="ts">
import type { DefaultTheme } from 'vitepress/theme'
import { computed } from 'vue'
import VPTeamMembersItem from './VPTeamMembersItem.vue'
interface Props {
size?: 'small' | 'medium'
members: DefaultTheme.TeamMember[]
}
const props = withDefaults(defineProps<Props>(), {
size: 'medium'
})
const classes = computed(() => [props.size, `count-${props.members.length}`])
</script>
<template>
<div class="VPTeamMembers" :class="classes">
<div class="container">
<div v-for="member in members" :key="member.name" class="item">
<VPTeamMembersItem :size :member />
</div>
</div>
</div>
</template>
<style scoped>
.VPTeamMembers.small .container {
grid-template-columns: repeat(auto-fit, minmax(224px, 1fr));
}
.VPTeamMembers.small.count-1 .container {
max-width: 276px;
}
.VPTeamMembers.small.count-2 .container {
max-width: calc(276px * 2 + 24px);
}
.VPTeamMembers.small.count-3 .container {
max-width: calc(276px * 3 + 24px * 2);
}
.VPTeamMembers.medium .container {
grid-template-columns: repeat(auto-fit, minmax(256px, 1fr));
}
@media (min-width: 375px) {
.VPTeamMembers.medium .container {
grid-template-columns: repeat(auto-fit, minmax(288px, 1fr));
}
}
.VPTeamMembers.medium.count-1 .container {
max-width: 368px;
}
.VPTeamMembers.medium.count-2 .container {
max-width: calc(368px * 2 + 24px);
}
.container {
display: grid;
gap: 24px;
margin: 0 auto;
max-width: 1152px;
}
</style>

View File

@@ -0,0 +1,225 @@
<script setup lang="ts">
import type { DefaultTheme } from 'vitepress/theme'
import VPLink from './VPLink.vue'
import VPSocialLinks from './VPSocialLinks.vue'
interface Props {
size?: 'small' | 'medium'
member: DefaultTheme.TeamMember
}
withDefaults(defineProps<Props>(), {
size: 'medium'
})
</script>
<template>
<article class="VPTeamMembersItem" :class="[size]">
<div class="profile">
<figure class="avatar">
<img class="avatar-img" :src="member.avatar" :alt="member.name" />
</figure>
<div class="data">
<h1 class="name">
{{ member.name }}
</h1>
<p v-if="member.title || member.org" class="affiliation">
<span v-if="member.title" class="title">
{{ member.title }}
</span>
<span v-if="member.title && member.org" class="at"> @ </span>
<VPLink
v-if="member.org"
class="org"
:class="{ link: member.orgLink }"
:href="member.orgLink"
no-icon
>
{{ member.org }}
</VPLink>
</p>
<p v-if="member.desc" class="desc" v-html="member.desc" />
<div v-if="member.links" class="links">
<VPSocialLinks :links="member.links" :me="false" />
</div>
</div>
</div>
<div v-if="member.sponsor" class="sp">
<VPLink class="sp-link" :href="member.sponsor" no-icon>
<span class="vpi-heart sp-icon" /> {{ member.actionText || 'Sponsor' }}
</VPLink>
</div>
</article>
</template>
<style scoped>
.VPTeamMembersItem {
display: flex;
flex-direction: column;
gap: 2px;
border-radius: 12px;
width: 100%;
height: 100%;
overflow: hidden;
}
.VPTeamMembersItem.small .profile {
padding: 32px;
}
.VPTeamMembersItem.small .data {
padding-top: 20px;
}
.VPTeamMembersItem.small .avatar {
width: 64px;
height: 64px;
}
.VPTeamMembersItem.small .name {
line-height: 24px;
font-size: 16px;
}
.VPTeamMembersItem.small .affiliation {
padding-top: 4px;
line-height: 20px;
font-size: 14px;
}
.VPTeamMembersItem.small .desc {
padding-top: 12px;
line-height: 20px;
font-size: 14px;
}
.VPTeamMembersItem.small .links {
margin: 0 -16px -20px;
padding: 10px 0 0;
}
.VPTeamMembersItem.medium .profile {
padding: 48px 32px;
}
.VPTeamMembersItem.medium .data {
padding-top: 24px;
text-align: center;
}
.VPTeamMembersItem.medium .avatar {
width: 96px;
height: 96px;
}
.VPTeamMembersItem.medium .name {
letter-spacing: 0.15px;
line-height: 28px;
font-size: 20px;
}
.VPTeamMembersItem.medium .affiliation {
padding-top: 4px;
font-size: 16px;
}
.VPTeamMembersItem.medium .desc {
padding-top: 16px;
max-width: 288px;
font-size: 16px;
}
.VPTeamMembersItem.medium .links {
margin: 0 -16px -12px;
padding: 16px 12px 0;
}
.profile {
flex-grow: 1;
background-color: var(--vp-c-bg-soft);
}
.data {
text-align: center;
}
.avatar {
position: relative;
flex-shrink: 0;
margin: 0 auto;
border-radius: 50%;
box-shadow: var(--vp-shadow-3);
}
.avatar-img {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
border-radius: 50%;
object-fit: cover;
}
.name {
margin: 0;
font-weight: 600;
}
.affiliation {
margin: 0;
font-weight: 500;
color: var(--vp-c-text-2);
}
.org.link {
color: var(--vp-c-text-2);
transition: color 0.25s;
}
.org.link:hover {
color: var(--vp-c-brand-1);
}
.desc {
margin: 0 auto;
}
.desc :deep(a) {
font-weight: 500;
color: var(--vp-c-brand-1);
text-decoration-style: dotted;
transition: color 0.25s;
}
.links {
display: flex;
justify-content: center;
height: 56px;
}
.sp-link {
display: flex;
justify-content: center;
align-items: center;
text-align: center;
padding: 16px;
font-size: 14px;
font-weight: 500;
color: var(--vp-c-sponsor);
background-color: var(--vp-c-bg-soft);
transition: color 0.25s, background-color 0.25s;
}
.sp .sp-link.link:hover,
.sp .sp-link.link:focus {
outline: none;
color: var(--vp-c-white);
background-color: var(--vp-c-sponsor);
}
.sp-icon {
margin-right: 8px;
font-size: 16px;
}
</style>

View File

@@ -0,0 +1,58 @@
<template>
<div class="VPTeamPage">
<slot />
</div>
</template>
<style scoped>
.VPTeamPage {
margin: 96px 0;
}
@media (min-width: 768px) {
.VPTeamPage {
margin: 128px 0;
}
}
.VPHome :slotted(.VPTeamPageTitle) {
border-top: 1px solid var(--vp-c-gutter);
padding-top: 88px !important;
}
:slotted(.VPTeamPageSection + .VPTeamPageSection),
:slotted(.VPTeamMembers + .VPTeamPageSection) {
margin-top: 64px;
}
:slotted(.VPTeamMembers + .VPTeamMembers) {
margin-top: 24px;
}
@media (min-width: 768px) {
:slotted(.VPTeamPageTitle + .VPTeamPageSection) {
margin-top: 16px;
}
:slotted(.VPTeamPageSection + .VPTeamPageSection),
:slotted(.VPTeamMembers + .VPTeamPageSection) {
margin-top: 96px;
}
}
:slotted(.VPTeamMembers) {
padding: 0 24px;
}
@media (min-width: 768px) {
:slotted(.VPTeamMembers) {
padding: 0 48px;
}
}
@media (min-width: 960px) {
:slotted(.VPTeamMembers) {
padding: 0 64px;
}
}
</style>

View File

@@ -0,0 +1,77 @@
<template>
<section class="VPTeamPageSection">
<div class="title">
<div class="title-line" />
<h2 v-if="$slots.title" class="title-text">
<slot name="title" />
</h2>
</div>
<p v-if="$slots.lead" class="lead">
<slot name="lead" />
</p>
<div v-if="$slots.members" class="members">
<slot name="members" />
</div>
</section>
</template>
<style scoped>
.VPTeamPageSection {
padding: 0 32px;
}
@media (min-width: 768px) {
.VPTeamPageSection {
padding: 0 48px;
}
}
@media (min-width: 960px) {
.VPTeamPageSection {
padding: 0 64px;
}
}
.title {
position: relative;
margin: 0 auto;
max-width: 1152px;
text-align: center;
color: var(--vp-c-text-2);
}
.title-line {
position: absolute;
top: 16px;
left: 0;
width: 100%;
height: 1px;
background-color: var(--vp-c-divider);
}
.title-text {
position: relative;
display: inline-block;
padding: 0 24px;
letter-spacing: 0;
line-height: 32px;
font-size: 20px;
font-weight: 500;
background-color: var(--vp-c-bg);
}
.lead {
margin: 0 auto;
max-width: 480px;
padding-top: 12px;
text-align: center;
line-height: 24px;
font-size: 16px;
font-weight: 500;
color: var(--vp-c-text-2);
}
.members {
padding-top: 40px;
}
</style>

View File

@@ -0,0 +1,63 @@
<template>
<div class="VPTeamPageTitle">
<h1 v-if="$slots.title" class="title">
<slot name="title" />
</h1>
<p v-if="$slots.lead" class="lead">
<slot name="lead" />
</p>
</div>
</template>
<style scoped>
.VPTeamPageTitle {
padding: 48px 32px;
text-align: center;
}
@media (min-width: 768px) {
.VPTeamPageTitle {
padding: 64px 48px 48px;
}
}
@media (min-width: 960px) {
.VPTeamPageTitle {
padding: 80px 64px 48px;
}
}
.title {
letter-spacing: 0;
line-height: 44px;
font-size: 36px;
font-weight: 500;
}
@media (min-width: 768px) {
.title {
letter-spacing: -0.5px;
line-height: 56px;
font-size: 48px;
}
}
.lead {
margin: 0 auto;
max-width: 512px;
padding-top: 12px;
line-height: 24px;
font-size: 16px;
font-weight: 500;
color: var(--vp-c-text-2);
}
@media (min-width: 768px) {
.lead {
max-width: 592px;
letter-spacing: 0.15px;
line-height: 28px;
font-size: 20px;
}
}
</style>

View File

@@ -0,0 +1,8 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewBox="0 0 24 24">
<path d="M21,11H3c-0.6,0-1-0.4-1-1s0.4-1,1-1h18c0.6,0,1,0.4,1,1S21.6,11,21,11z" />
<path d="M21,7H3C2.4,7,2,6.6,2,6s0.4-1,1-1h18c0.6,0,1,0.4,1,1S21.6,7,21,7z" />
<path d="M21,15H3c-0.6,0-1-0.4-1-1s0.4-1,1-1h18c0.6,0,1,0.4,1,1S21.6,15,21,15z" />
<path d="M21,19H3c-0.6,0-1-0.4-1-1s0.4-1,1-1h18c0.6,0,1,0.4,1,1S21.6,19,21,19z" />
</svg>
</template>

View File

@@ -0,0 +1,8 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewBox="0 0 24 24">
<path d="M17,11H3c-0.6,0-1-0.4-1-1s0.4-1,1-1h14c0.6,0,1,0.4,1,1S17.6,11,17,11z" />
<path d="M21,7H3C2.4,7,2,6.6,2,6s0.4-1,1-1h18c0.6,0,1,0.4,1,1S21.6,7,21,7z" />
<path d="M21,15H3c-0.6,0-1-0.4-1-1s0.4-1,1-1h18c0.6,0,1,0.4,1,1S21.6,15,21,15z" />
<path d="M17,19H3c-0.6,0-1-0.4-1-1s0.4-1,1-1h14c0.6,0,1,0.4,1,1S17.6,19,17,19z" />
</svg>
</template>

View File

@@ -0,0 +1,8 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewBox="0 0 24 24">
<path d="M21,11H7c-0.6,0-1-0.4-1-1s0.4-1,1-1h14c0.6,0,1,0.4,1,1S21.6,11,21,11z" />
<path d="M21,7H3C2.4,7,2,6.6,2,6s0.4-1,1-1h18c0.6,0,1,0.4,1,1S21.6,7,21,7z" />
<path d="M21,15H3c-0.6,0-1-0.4-1-1s0.4-1,1-1h18c0.6,0,1,0.4,1,1S21.6,15,21,15z" />
<path d="M21,19H7c-0.6,0-1-0.4-1-1s0.4-1,1-1h14c0.6,0,1,0.4,1,1S21.6,19,21,19z" />
</svg>
</template>

View File

@@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M19,11H7.4l5.3-5.3c0.4-0.4,0.4-1,0-1.4s-1-0.4-1.4,0l-7,7c-0.1,0.1-0.2,0.2-0.2,0.3c-0.1,0.2-0.1,0.5,0,0.8c0.1,0.1,0.1,0.2,0.2,0.3l7,7c0.2,0.2,0.5,0.3,0.7,0.3s0.5-0.1,0.7-0.3c0.4-0.4,0.4-1,0-1.4L7.4,13H19c0.6,0,1-0.4,1-1S19.6,11,19,11z"
/>
</svg>
</template>

View File

@@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M19.9,12.4c0.1-0.2,0.1-0.5,0-0.8c-0.1-0.1-0.1-0.2-0.2-0.3l-7-7c-0.4-0.4-1-0.4-1.4,0s-0.4,1,0,1.4l5.3,5.3H5c-0.6,0-1,0.4-1,1s0.4,1,1,1h11.6l-5.3,5.3c-0.4,0.4-0.4,1,0,1.4c0.2,0.2,0.5,0.3,0.7,0.3s0.5-0.1,0.7-0.3l7-7C19.8,12.6,19.9,12.5,19.9,12.4z"
/>
</svg>
</template>

View File

@@ -0,0 +1,5 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewBox="0 0 24 24">
<path d="M12,16c-0.3,0-0.5-0.1-0.7-0.3l-6-6c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l5.3,5.3l5.3-5.3c0.4-0.4,1-0.4,1.4,0s0.4,1,0,1.4l-6,6C12.5,15.9,12.3,16,12,16z" />
</svg>
</template>

View File

@@ -0,0 +1,5 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewBox="0 0 24 24">
<path d="M15,19c-0.3,0-0.5-0.1-0.7-0.3l-6-6c-0.4-0.4-0.4-1,0-1.4l6-6c0.4-0.4,1-0.4,1.4,0s0.4,1,0,1.4L10.4,12l5.3,5.3c0.4,0.4,0.4,1,0,1.4C15.5,18.9,15.3,19,15,19z" />
</svg>
</template>

View File

@@ -0,0 +1,5 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewBox="0 0 24 24">
<path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z" />
</svg>
</template>

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