This commit is contained in:
NoryiE
2025-02-16 14:12:49 +00:00
parent c6a89e5b35
commit e0aeb9b06e
2737 changed files with 5220 additions and 1039045 deletions

21
node_modules/vitepress/LICENSE generated vendored
View File

@@ -1,21 +0,0 @@
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.

29
node_modules/vitepress/README.md generated vendored
View File

@@ -1,29 +0,0 @@
# VitePress (RC: release candidate) 📝💨
[![Test](https://github.com/vuejs/vitepress/workflows/Test/badge.svg)](https://github.com/vuejs/vitepress/actions)
[![npm](https://img.shields.io/npm/v/vitepress)](https://www.npmjs.com/package/vitepress)
[![chat](https://img.shields.io/badge/chat-discord-blue?logo=discord)](https://chat.vuejs.org)
---
VitePress is [VuePress](https://vuepress.vuejs.org)' spiritual successor, built on top of [vite](https://github.com/vitejs/vite).
Currently, it is in the `release candidate` stage. It is already suitable for out-of-the-box documentation use. We do not plan to introduce any breaking changes from here on until the stable release.
## 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

View File

@@ -1,2 +0,0 @@
#!/usr/bin/env node
import('../dist/node/cli.js')

5
node_modules/vitepress/client.d.ts generated vendored
View File

@@ -1,5 +0,0 @@
// 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'

View File

@@ -1,10 +0,0 @@
import { defineComponent, ref, onMounted } 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

@@ -1,22 +0,0 @@
import { defineComponent, h } from 'vue';
import { useData, useRoute } from 'vitepress';
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 { site } = useData();
return () => h(props.as, site.value.contentProps ?? { style: { position: 'relative' } }, [
route.component
? h(route.component, {
onVnodeMounted: runCbs,
onVnodeUpdated: runCbs
})
: '404 Page Not Found'
]);
}
});

View File

@@ -1,40 +0,0 @@
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');
});
el.children[0].classList.add('active');
});
});
}
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');
next.classList.add('active');
const label = group?.querySelector(`label[for="${el.id}"]`);
label?.scrollIntoView({ block: 'nearest' });
}
});
}
}

View File

@@ -1,71 +0,0 @@
import { inBrowser } from 'vitepress';
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;
if (!parent || !sibling) {
return;
}
const isShell = /language-(shellscript|shell|bash|sh|zsh)/.test(parent.className);
let text = '';
sibling
.querySelectorAll('span.line:not(.diff.remove)')
.forEach((node) => (text += (node.textContent || '') + '\n'));
text = text.slice(0, -1);
if (isShell) {
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

@@ -1,61 +0,0 @@
import { watchEffect } from 'vue';
import { createTitle, mergeHead } from '../../shared';
export function useUpdateHead(route, siteDataByRouteRef) {
let managedHeadTags = [];
let isFirstUpdate = true;
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;
return;
}
managedHeadTags.forEach((el) => document.head.removeChild(el));
managedHeadTags = [];
newTags.forEach((headConfig) => {
const el = createHeadElement(headConfig);
document.head.appendChild(el);
managedHeadTags.push(el);
});
};
watchEffect(() => {
const pageData = route.data;
const siteData = siteDataByRouteRef.value;
const pageDescription = pageData && pageData.description;
const frontmatterHead = (pageData && pageData.frontmatter.head) || [];
// update title and description
document.title = createTitle(siteData, pageData);
const description = pageDescription || siteData.description;
let metaDescriptionElement = document.querySelector(`meta[name=description]`);
if (metaDescriptionElement) {
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) {
// 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

@@ -1,99 +0,0 @@
// Customized pre-fetch for page chunks based on
// https://github.com/GoogleChromeLabs/quicklink
import { useRoute } from '../router';
import { onMounted, onUnmounted, watch } from 'vue';
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();
});
}

View File

@@ -1,53 +0,0 @@
import siteData from '@siteData';
import { useDark } from '@vueuse/core';
import { computed, inject, readonly, ref, shallowRef } from 'vue';
import { APPEARANCE_KEY, createTitle, resolveSiteDataByRoute } from '../shared';
export const dataSymbol = Symbol();
// site data is a singleton
export const siteDataRef = shallowRef((import.meta.env.PROD ? siteData : readonly(siteData)));
// hmr
if (import.meta.hot) {
import.meta.hot.accept('/@siteData', (m) => {
if (m) {
siteDataRef.value = m.default;
}
});
}
// 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
? useDark({
storageKey: APPEARANCE_KEY,
initialValue: () => typeof appearance === 'string' ? appearance : 'auto',
...(typeof appearance === 'object' ? appearance : {})
})
: ref(false);
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(() => site.value.dir),
localeIndex: computed(() => site.value.localeIndex || 'root'),
title: computed(() => {
return createTitle(site.value, route.data);
}),
description: computed(() => {
return route.data.description || site.value.description;
}),
isDark
};
}
export function useData() {
const data = inject(dataSymbol);
if (!data) {
throw new Error('vitepress data not properly injected in app');
}
return data;
}

View File

@@ -1,28 +0,0 @@
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
});
});
});
};

View File

@@ -1,133 +0,0 @@
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 } = useData();
// change the language on the HTML element based on the current lang
onMounted(() => {
watchEffect(() => {
document.documentElement.lang = site.value.lang;
document.documentElement.dir = site.value.dir;
});
});
if (import.meta.env.PROD) {
// 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() {
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;
let initialPath;
return createRouter((path) => {
let pageFilePath = pathToFile(path);
let pageModule = null;
if (pageFilePath) {
if (isInitialPageLoad) {
initialPath = pageFilePath;
}
// use lean build if this is the initial page load or navigating back
// to the initial loaded path (the static vnodes already adopted the
// static content on that load so no need to re-fetch the page)
if (isInitialPageLoad || initialPath === pageFilePath) {
pageFilePath = pageFilePath.replace(/\.js$/, '.lean.js');
}
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().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) {
const target = document.getElementById(decodeURIComponent(location.hash).slice(1));
if (target) {
scrollTo(target, location.hash);
}
}
});
});
}

View File

@@ -1,266 +0,0 @@
import { reactive, inject, markRaw, nextTick, readonly } from 'vue';
import { notFoundPageData } from '../shared';
import { inBrowser, withBase } from './utils';
import { siteDataRef } from './data';
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: '/',
component: null,
data: notFoundPageData
});
export function createRouter(loadPageModule, fallbackComponent) {
const route = reactive(getDefaultRoute());
const router = {
route,
go
};
async function go(href = inBrowser ? location.href : '/') {
href = normalizeHref(href);
if ((await router.onBeforeRouteChange?.(href)) === false)
return;
updateHistory(href);
await loadPage(href);
await router.onAfterRouteChanged?.(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}`);
}
route.path = inBrowser ? pendingPath : withBase(pendingPath);
route.component = markRaw(comp);
route.data = import.meta.env.PROD
? markRaw(__pageData)
: readonly(__pageData);
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(null, '', href);
}
if (targetLoc.hash && !scrollPosition) {
let target = null;
try {
target = document.getElementById(decodeURIComponent(targetLoc.hash).slice(1));
}
catch (e) {
console.warn(e);
}
if (target) {
scrollTo(target, targetLoc.hash);
return;
}
}
window.scrollTo(0, 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;
route.data = notFoundPageData;
}
}
}
if (inBrowser) {
window.addEventListener('click', (e) => {
// temporary fix for docsearch action buttons
const button = e.target.closest('button');
if (button)
return;
const link = e.target.closest('a');
if (link &&
!link.closest('.vp-raw') &&
(link instanceof SVGElement || !link.download)) {
const { target } = link;
const { href, origin, pathname, hash, search } = new URL(link.href instanceof SVGAnimatedString
? link.href.animVal
: link.href, link.baseURI);
const currentUrl = window.location;
const extMatch = pathname.match(/\.\w+$/);
// only intercept inbound links
if (!e.ctrlKey &&
!e.shiftKey &&
!e.altKey &&
!e.metaKey &&
!target &&
origin === currentUrl.origin &&
// don't intercept if non-html extension is present
!(extMatch && extMatch[0] !== '.html')) {
e.preventDefault();
if (pathname === currentUrl.pathname &&
search === currentUrl.search) {
// scroll between hash anchors in the same page
// avoid duplicate history entries when the hash is same
if (hash !== currentUrl.hash) {
history.pushState(null, '', hash);
// still emit the event so we can listen to it in themes
window.dispatchEvent(new Event('hashchange'));
}
if (hash) {
// use smooth scroll when clicking on header anchor links
scrollTo(link, hash, link.classList.contains('header-anchor'));
}
else {
updateHistory(href);
window.scrollTo(0, 0);
}
}
else {
go(href);
}
}
}
}, { capture: true });
window.addEventListener('popstate', (e) => {
loadPage(normalizeHref(location.href), (e.state && e.state.scrollPosition) || 0);
});
window.addEventListener('hashchange', (e) => {
e.preventDefault();
});
}
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(el, hash, smooth = false) {
let target = null;
try {
target = el.classList.contains('header-anchor')
? el
: document.getElementById(decodeURIComponent(hash).slice(1));
}
catch (e) {
console.warn(e);
}
if (target) {
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;
}
}
}
const targetPadding = parseInt(window.getComputedStyle(target).paddingTop, 10);
const targetTop = window.scrollY +
target.getBoundingClientRect().top -
offset +
targetPadding;
function scrollToTarget() {
// only smooth scroll if distance is smaller than screen height.
if (!smooth || Math.abs(targetTop - window.scrollY) > window.innerHeight)
window.scrollTo(0, targetTop);
else
window.scrollTo({ left: 0, top: targetTop, behavior: 'smooth' });
}
requestAnimationFrame(scrollToTarget);
}
}
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;
}
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 updateHistory(href) {
if (inBrowser && href !== normalizeHref(location.href)) {
// save scroll position before changing url
history.replaceState({ scrollPosition: window.scrollY }, document.title);
history.pushState(null, '', href);
}
}
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;
}

View File

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

View File

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

View File

@@ -1,85 +0,0 @@
import { siteDataRef } from './data';
import { inBrowser, EXTERNAL_URL_RE, sanitizeFileName } from '../shared';
import { h, onMounted, onUnmounted, shallowRef } from 'vue';
export { 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);
onUnmounted(() => {
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);
}
};
}

View File

@@ -1,120 +0,0 @@
import * as vue from 'vue';
import { Component, InjectionKey, Ref, 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;
interface Route {
path: string;
data: PageData;
component: Component | null;
}
interface Router {
/**
* Current route.
*/
route: Route;
/**
* Navigate to a new URL.
*/
go: (to?: string) => 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 route changes.
*/
onAfterRouteChanged?: (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>;
isDark: Ref<boolean>;
dir: Ref<string>;
localeIndex: 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 const Content: vue.DefineComponent<{
as: {
type: (ObjectConstructor | StringConstructor)[];
default: string;
};
}, () => vue.VNode<vue.RendererNode, vue.RendererElement, {
[key: string]: any;
}>, unknown, {}, {}, vue.ComponentOptionsMixin, vue.ComponentOptionsMixin, {}, string, vue.VNodeProps & vue.AllowedComponentProps & vue.ComponentCustomProps, Readonly<vue.ExtractPropTypes<{
as: {
type: (ObjectConstructor | StringConstructor)[];
default: string;
};
}>>, {
as: string | Record<string, any>;
}, {}>;
export { Content, type EnhanceAppContext, type Route, type Router, type Theme, type VitePressData, dataSymbol, defineClientComponent, inBrowser, onContentUpdated, useData, useRoute, useRouter, withBase };

View File

@@ -1,9 +0,0 @@
// 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 { useData, dataSymbol } from './app/data';
export { useRoute, useRouter } from './app/router';
// utilities
export { inBrowser, onContentUpdated, defineClientComponent, withBase } from './app/utils';
// components
export { Content } from './app/components/Content';

View File

@@ -1,110 +0,0 @@
export const EXTERNAL_URL_RE = /^[a-z]+:/i;
export const APPEARANCE_KEY = 'vitepress-theme-appearance';
export const HASH_RE = /#.*$/;
export const EXT_RE = /(index)?\.(md|html)$/;
export const inBrowser = typeof document !== 'undefined';
export const notFoundPageData = {
relativePath: '',
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_RE, '').replace(EXT_RE, '');
}
export function isExternal(path) {
return EXTERNAL_URL_RE.test(path);
}
/**
* this merges the locales data to the main data by the route
*/
export function resolveSiteDataByRoute(siteData, relativePath) {
const localeIndex = Object.keys(siteData.locales).find((key) => key !== 'root' &&
!isExternal(key) &&
isActive(relativePath, `/${key}/`, true)) || 'root';
return Object.assign({}, siteData, {
localeIndex,
lang: siteData.locales[localeIndex]?.lang ?? siteData.lang,
dir: siteData.locales[localeIndex]?.dir ?? siteData.dir,
title: siteData.locales[localeIndex]?.title ?? siteData.title,
titleTemplate: siteData.locales[localeIndex]?.titleTemplate ?? siteData.titleTemplate,
description: siteData.locales[localeIndex]?.description ?? siteData.description,
head: mergeHead(siteData.head, siteData.locales[localeIndex]?.head ?? []),
themeConfig: {
...siteData.themeConfig,
...siteData.locales[localeIndex]?.themeConfig
}
});
}
/**
* 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);
return `${title}${templateString}`;
}
function createTitleTemplate(siteTitle, template) {
if (template === false) {
return '';
}
if (template === true || template === undefined) {
return ` | ${siteTitle}`;
}
if (siteTitle === template) {
return '';
}
return ` | ${template}`;
}
function hasTag(head, tag) {
const [tagType, tagAttrs] = tag;
if (tagType !== 'meta')
return false;
const keyAttr = Object.entries(tagAttrs)[0]; // First key
if (keyAttr == null)
return false;
return head.some(([type, attrs]) => type === tagType && attrs[keyAttr[0]] === keyAttr[1]);
}
export function mergeHead(prev, curr) {
return [...prev.filter((tagAttrs) => !hasTag(curr, tagAttrs)), ...curr];
}
// 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, '/');
}

View File

@@ -1,91 +0,0 @@
<script setup lang="ts">
import { useRoute } from 'vitepress'
import { computed, provide, useSlots, watch } 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 { useCloseSidebarOnEscape, useSidebar } from './composables/sidebar'
const {
isOpen: isSidebarOpen,
open: openSidebar,
close: closeSidebar
} = useSidebar()
const route = useRoute()
watch(() => route.path, closeSidebar)
useCloseSidebarOnEscape(isSidebarOpen, closeSidebar)
const { frontmatter } = useData()
const slots = useSlots()
const heroImageSlotExists = computed(() => !!slots['home-hero-image'])
provide('hero-image-slot-exists', 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><slot name="home-hero-info" /></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

@@ -1,109 +0,0 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { withBase } from 'vitepress'
import { useData } from './composables/data'
import { useLangs } from './composables/langs'
const { site, theme } = useData()
const { localeLinks } = useLangs({ removeCurrent: false })
const root = ref('/')
onMounted(() => {
const path = window.location.pathname
.replace(site.value.base, '')
.replace(/(^.*?\/).*$/, '/$1')
if (localeLinks.value.length) {
root.value =
localeLinks.value.find(({ link }) => link.startsWith(path))?.link ||
localeLinks.value[0].link
}
})
</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(root)"
: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

@@ -1,100 +0,0 @@
<script setup lang="ts">
import type { DefaultTheme } from 'vitepress/theme'
import docsearch from '@docsearch/js'
import { onMounted, watch } from 'vue'
import { useRouter, useRoute } from 'vitepress'
import { useData } from '../composables/data'
const props = defineProps<{
algolia: DefaultTheme.AlgoliaSearchOptions
}>()
const router = useRouter()
const route = useRoute()
const { site, localeIndex, lang } = useData()
type DocSearchProps = Parameters<typeof docsearch>[0]
onMounted(update)
watch(localeIndex, update)
function update() {
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}`
]
initialize({
...options,
searchParameters: {
...options.searchParameters,
facetFilters
}
})
}
function initialize(userOptions: DefaultTheme.AlgoliaSearchOptions) {
const options = Object.assign<{}, {}, DocSearchProps>({}, userOptions, {
container: '#docsearch',
navigator: {
navigate({ itemUrl }) {
const { pathname: hitPathname } = new URL(
window.location.origin + itemUrl
)
// router doesn't handle same-page navigation so we use the native
// browser location API for anchor navigation
if (route.path === hitPathname) {
window.location.assign(window.location.origin + itemUrl)
} else {
router.go(itemUrl)
}
}
},
transformItems(items) {
return items.map((item) => {
return Object.assign({}, item, {
url: getRelativePath(item.url)
})
})
},
// @ts-expect-error vue-tsc thinks this should return Vue JSX but it returns the required React one
hitComponent({ hit, children }) {
return {
__v: null,
type: 'a',
ref: undefined,
constructor: undefined,
key: undefined,
props: { href: hit.url, children }
}
}
})
docsearch(options)
}
function getRelativePath(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

@@ -1,41 +0,0 @@
<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

@@ -1,75 +0,0 @@
<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 scoped>
.VPBadge {
display: inline-block;
margin-left: 2px;
border: 1px solid transparent;
border-radius: 12px;
padding: 0 10px;
line-height: 22px;
font-size: 12px;
font-weight: 500;
transform: translateY(-2px);
}
.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

@@ -1,121 +0,0 @@
<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
}
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="isExternal ? '_blank' : undefined"
:rel="isExternal ? 'noreferrer' : undefined"
>
{{ text }}
</component>
</template>
<style scoped>
.VPButton {
display: inline-block;
border: 1px solid transparent;
text-align: center;
font-weight: 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

@@ -1,109 +0,0 @@
<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

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

View File

@@ -1,210 +0,0 @@
<script setup lang="ts">
import { useRoute } from 'vitepress'
import { computed } from 'vue'
import { useData } from '../composables/data'
import { useSidebar } from '../composables/sidebar'
import VPDocAside from './VPDocAside.vue'
import VPDocFooter from './VPDocFooter.vue'
import VPDocOutlineDropdown from './VPDocOutlineDropdown.vue'
const { theme } = useData()
const route = useRoute()
const { hasSidebar, hasAside, leftAside } = useSidebar()
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" />
<VPDocOutlineDropdown />
<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%;
}
.VPDoc .VPDocOutlineDropdown {
display: none;
}
@media (min-width: 960px) and (max-width: 1279px) {
.VPDoc .VPDocOutlineDropdown {
display: block;
}
}
@media (min-width: 768px) {
.VPDoc {
padding: 48px 32px 128px;
}
}
@media (min-width: 960px) {
.VPDoc {
padding: 32px 32px 0;
}
.VPDoc:not(.has-sidebar) .container {
display: flex;
justify-content: center;
max-width: 992px;
}
.VPDoc:not(.has-sidebar) .content {
max-width: 752px;
}
}
@media (min-width: 1280px) {
.VPDoc .container {
display: flex;
justify-content: center;
}
.VPDoc .aside {
display: block;
}
}
@media (min-width: 1440px) {
.VPDoc:not(.has-sidebar) .content {
max-width: 784px;
}
.VPDoc:not(.has-sidebar) .container {
max-width: 1104px;
}
}
.container {
margin: 0 auto;
width: 100%;
}
.aside {
position: relative;
display: none;
order: 2;
flex-grow: 1;
padding-left: 32px;
width: 100%;
max-width: 256px;
}
.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) + 32px);
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%);
}
.aside-content {
display: flex;
flex-direction: column;
min-height: calc(100vh - (var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 32px));
padding-bottom: 32px;
}
.content {
position: relative;
margin: 0 auto;
width: 100%;
}
@media (min-width: 960px) {
.content {
padding: 0 32px 128px;
}
}
@media (min-width: 1280px) {
.content {
order: 1;
margin: 0;
min-width: 640px;
}
}
.content-container {
margin: 0 auto;
}
.VPDoc.has-aside .content-container {
max-width: 688px;
}
.external-link-icon-enabled :is(.vp-doc a[href*='://'], .vp-doc a[target='_blank'])::after {
content: '';
color: currentColor;
}
</style>

View File

@@ -1,46 +0,0 @@
<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

@@ -1,18 +0,0 @@
<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

@@ -1,88 +0,0 @@
<script setup lang="ts">
import { onContentUpdated } from 'vitepress'
import { ref, shallowRef } from 'vue'
import { useData } from '../composables/data'
import {
getHeaders,
resolveTitle,
useActiveAnchor,
type MenuItem
} from '../composables/outline'
import VPDocOutlineItem from './VPDocOutlineItem.vue'
const { frontmatter, theme } = useData()
const headers = shallowRef<MenuItem[]>([])
onContentUpdated(() => {
headers.value = getHeaders(frontmatter.value.outline ?? theme.value.outline)
})
const container = ref()
const marker = ref()
useActiveAnchor(container, marker)
</script>
<template>
<div
class="VPDocAsideOutline"
:class="{ 'has-outline': headers.length > 0 }"
ref="container"
role="navigation"
>
<div class="content">
<div class="outline-marker" ref="marker" />
<div class="outline-title" role="heading" aria-level="2">{{ resolveTitle(theme) }}</div>
<nav aria-labelledby="doc-outline-aria-label">
<span class="visually-hidden" id="doc-outline-aria-label">
Table of Contents for current page
</span>
<VPDocOutlineItem :headers="headers" :root="true" />
</nav>
</div>
</div>
</template>
<style scoped>
.VPDocAsideOutline {
display: none;
}
.VPDocAsideOutline.has-outline {
display: block;
}
.content {
position: relative;
border-left: 1px solid var(--vp-c-divider);
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 {
letter-spacing: 0.4px;
line-height: 28px;
font-size: 13px;
font-weight: 600;
}
</style>

View File

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

View File

@@ -1,150 +0,0 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useData } from '../composables/data'
import { normalizeLink } from '../support/utils'
import { useEditLink } from '../composables/edit-link'
import { usePrevNext } from '../composables/prev-next'
import VPIconEdit from './icons/VPIconEdit.vue'
import VPLink from './VPLink.vue'
import VPDocFooterLastUpdated from './VPDocFooterLastUpdated.vue'
const { theme, page, frontmatter } = useData()
const editLink = useEditLink()
const control = usePrevNext()
const hasEditLink = computed(() => {
return theme.value.editLink && frontmatter.value.editLink !== false
})
const hasLastUpdated = computed(() => {
return page.value.lastUpdated && frontmatter.value.lastUpdated !== false
})
const showFooter = computed(() => {
return hasEditLink.value || hasLastUpdated.value || control.value.prev || control.value.next
})
</script>
<template>
<footer v-if="showFooter" class="VPDocFooter">
<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">
<VPIconEdit class="edit-link-icon" aria-label="edit 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">
<div class="pager">
<a v-if="control.prev?.link" class="pager-link prev" :href="normalizeLink(control.prev.link)">
<span class="desc" v-html="theme.docFooter?.prev || 'Previous page'"></span>
<span class="title" v-html="control.prev.text"></span>
</a>
</div>
<div class="pager">
<a v-if="control.next?.link" class="pager-link next" :href="normalizeLink(control.next.link)">
<span class="desc" v-html="theme.docFooter?.next || 'Next page'"></span>
<span class="title" v-html="control.next.text"></span>
</a>
</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;
width: 14px;
height: 14px;
fill: currentColor;
}
.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

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

View File

@@ -1,85 +0,0 @@
<script setup lang="ts">
import { ref, shallowRef } from 'vue'
import { useData } from '../composables/data'
import { getHeaders, resolveTitle, type MenuItem } from '../composables/outline'
import VPDocOutlineItem from './VPDocOutlineItem.vue'
import { onContentUpdated } from 'vitepress'
import VPIconChevronRight from './icons/VPIconChevronRight.vue'
const { frontmatter, theme } = useData()
const open = ref(false)
onContentUpdated(() => {
open.value = false
})
const headers = shallowRef<MenuItem[]>([])
onContentUpdated(() => {
headers.value = getHeaders(
frontmatter.value.outline ?? theme.value.outline
)
})
</script>
<template>
<div class="VPDocOutlineDropdown" v-if="headers.length > 0">
<button @click="open = !open" :class="{ open }">
{{ resolveTitle(theme) }}
<VPIconChevronRight class="icon" />
</button>
<div class="items" v-if="open">
<VPDocOutlineItem :headers="headers" />
</div>
</div>
</template>
<style scoped>
.VPDocOutlineDropdown {
margin-bottom: 48px;
}
.VPDocOutlineDropdown button {
display: block;
font-size: 14px;
font-weight: 500;
line-height: 24px;
border: 1px solid var(--vp-c-border);
padding: 4px 12px;
color: var(--vp-c-text-2);
background-color: var(--vp-c-default-soft);
border-radius: 8px;
transition: color 0.5s;
}
.VPDocOutlineDropdown button:hover {
color: var(--vp-c-text-1);
transition: color 0.25s;
}
.VPDocOutlineDropdown button.open {
color: var(--vp-c-text-1);
}
.icon {
display: inline-block;
vertical-align: middle;
width: 16px;
height: 16px;
fill: currentColor;
}
:deep(.outline-link) {
font-size: 14px;
font-weight: 400;
}
.open > .icon {
transform: rotate(90deg);
}
.items {
margin-top: 12px;
border-left: 1px solid var(--vp-c-divider);
}
</style>

View File

@@ -1,57 +0,0 @@
<script setup lang="ts">
import type { MenuItem } from '../composables/outline'
defineProps<{
headers: MenuItem[]
root?: boolean
}>()
function onClick({ target: el }: Event) {
const id = (el as HTMLAnchorElement).href!.split('#')[1]
const heading = document.getElementById(decodeURIComponent(id))
heading?.focus({ preventScroll: true })
}
</script>
<template>
<ul :class="root ? 'root' : 'nested'">
<li v-for="{ children, link, title } in headers">
<a class="outline-link" :href="link" @click="onClick" :title="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-left: 16px;
}
.outline-link {
display: block;
line-height: 28px;
color: var(--vp-c-text-2);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: color 0.5s;
font-weight: 400;
}
.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

@@ -1,128 +0,0 @@
<script setup lang="ts">
import type { DefaultTheme } from 'vitepress/theme'
import VPImage from './VPImage.vue'
import VPLink from './VPLink.vue'
import VPIconArrowRight from './icons/VPIconArrowRight.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="rel"
:target="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 }} <VPIconArrowRight class="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 {
display: inline-block;
margin-left: 6px;
width: 14px;
height: 14px;
fill: currentColor;
}
</style>

View File

@@ -1,121 +0,0 @@
<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

@@ -1,144 +0,0 @@
<script lang="ts" setup>
import { ref } from 'vue'
import { useFlyout } from '../composables/flyout'
import VPIconChevronDown from './icons/VPIconChevronDown.vue'
import VPIconMoreHorizontal from './icons/VPIconMoreHorizontal.vue'
import VPMenu from './VPMenu.vue'
defineProps<{
icon?: any
button?: string
label?: string
items?: any[]
}>()
const open = ref(false)
const el = ref<HTMLElement>()
useFlyout({ el, onBlur })
function onBlur() {
open.value = false
}
</script>
<template>
<div
class="VPFlyout"
ref="el"
@mouseenter="open = true"
@mouseleave="open = false"
>
<button
type="button"
class="button"
aria-haspopup="true"
:aria-expanded="open"
:aria-label="label"
@click="open = !open"
>
<span v-if="button || icon" class="text">
<component v-if="icon" :is="icon" class="option-icon" />
<span v-if="button" v-html="button"></span>
<VPIconChevronDown class="text-icon" />
</span>
<VPIconMoreHorizontal v-else class="icon" />
</button>
<div class="menu">
<VPMenu :items="items">
<slot />
</VPMenu>
</div>
</div>
</template>
<style scoped>
.VPFlyout {
position: relative;
}
.VPFlyout:hover {
color: var(--vp-c-brand-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);
}
.VPFlyout:hover .menu,
.button[aria-expanded="true"] + .menu {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.button[aria-expanded="false"] + .menu {
opacity: 0;
visibility: hidden;
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;
width: 16px;
height: 16px;
fill: currentColor;
}
.text-icon {
margin-left: 4px;
width: 14px;
height: 14px;
fill: currentColor;
}
.icon {
width: 20px;
height: 20px;
fill: currentColor;
transition: fill 0.25s;
}
.menu {
position: absolute;
top: calc(var(--vp-nav-height) / 2 + 20px);
right: 0;
opacity: 0;
visibility: hidden;
transition: opacity 0.25s, visibility 0.25s, transform 0.25s;
}
</style>

View File

@@ -1,50 +0,0 @@
<script setup lang="ts">
import { useData } from '../composables/data'
import { useSidebar } from '../composables/sidebar'
const { theme, frontmatter } = useData()
const { hasSidebar } = useSidebar()
</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;
}
@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

@@ -1,329 +0,0 @@
<script setup lang="ts">
import { type Ref, inject } from 'vue'
import type { DefaultTheme } from 'vitepress/theme'
import VPButton from './VPButton.vue'
import VPImage from './VPImage.vue'
export interface HeroAction {
theme?: 'brand' | 'alt'
text: string
link: string
}
defineProps<{
name?: string
text?: string
tagline?: string
image?: DefaultTheme.ThemeableImage
actions?: HeroAction[]
}>()
const heroImageSlotExists = inject('hero-image-slot-exists') as Ref<boolean>
</script>
<template>
<div class="VPHero" :class="{ 'has-image': image || heroImageSlotExists }">
<div class="container">
<div class="main">
<slot name="home-hero-info">
<h1 v-if="name" class="name">
<span v-html="name" class="clip"></span>
</h1>
<p v-if="text" v-html="text" class="text"></p>
<p v-if="tagline" v-html="tagline" class="tagline"></p>
</slot>
<div v-if="actions" class="actions">
<div v-for="action in actions" :key="action.link" class="action">
<VPButton
tag="a"
size="medium"
:theme="action.theme"
:text="action.text"
:href="action.link"
/>
</div>
</div>
</div>
<div v-if="image || heroImageSlotExists" class="image">
<div class="image-container">
<div class="image-bg" />
<slot name="home-hero-image">
<VPImage v-if="image" class="image-src" :image="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;
}
}
.name,
.text {
max-width: 392px;
letter-spacing: -0.4px;
line-height: 40px;
font-size: 32px;
font-weight: 700;
white-space: pre-wrap;
}
.VPHero.has-image .name,
.VPHero.has-image .text {
margin: 0 auto;
}
.name {
color: var(--vp-home-hero-name-color);
}
.clip {
background: var(--vp-home-hero-name-background);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: var(--vp-home-hero-name-color);
}
@media (min-width: 640px) {
.name,
.text {
max-width: 576px;
line-height: 56px;
font-size: 48px;
}
}
@media (min-width: 960px) {
.name,
.text {
line-height: 64px;
font-size: 56px;
}
.VPHero.has-image .name,
.VPHero.has-image .text {
margin: 0;
}
}
.tagline {
padding-top: 8px;
max-width: 392px;
line-height: 28px;
font-size: 18px;
font-weight: 500;
white-space: pre-wrap;
color: var(--vp-c-text-2);
}
.VPHero.has-image .tagline {
margin: 0 auto;
}
@media (min-width: 640px) {
.tagline {
padding-top: 12px;
max-width: 576px;
line-height: 32px;
font-size: 20px;
}
}
@media (min-width: 960px) {
.tagline {
line-height: 36px;
font-size: 24px;
}
.VPHero.has-image .tagline {
margin: 0;
}
}
.actions {
display: flex;
flex-wrap: wrap;
margin: -6px;
padding-top: 24px;
}
.VPHero.has-image .actions {
justify-content: center;
}
@media (min-width: 640px) {
.actions {
padding-top: 32px;
}
}
@media (min-width: 960px) {
.VPHero.has-image .actions {
justify-content: flex-start;
}
}
.action {
flex-shrink: 0;
padding: 6px;
}
.image {
order: 1;
margin: -76px -24px -48px;
}
@media (min-width: 640px) {
.image {
margin: -108px -24px -48px;
}
}
@media (min-width: 960px) {
.image {
flex-grow: 1;
order: 2;
margin: 0;
min-height: 100%;
}
}
.image-container {
position: relative;
margin: 0 auto;
width: 320px;
height: 320px;
}
@media (min-width: 640px) {
.image-container {
width: 392px;
height: 392px;
}
}
@media (min-width: 960px) {
.image-container {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
/*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;
/*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

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

View File

@@ -1,14 +0,0 @@
<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

@@ -1,21 +0,0 @@
<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><slot name="home-hero-info" /></template>
<template #home-hero-image><slot name="home-hero-image" /></template>
</VPHero>
</template>

View File

@@ -1,92 +0,0 @@
<script setup lang="ts">
import VPIconHeart from './icons/VPIconHeart.vue'
import VPButton from './VPButton.vue'
import VPSponsors from './VPSponsors.vue'
export interface Sponsors {
tier: string
size?: 'medium' | 'big'
items: Sponsor[]
}
export interface Sponsor {
name: string
img: string
url: string
}
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"><VPIconHeart class="icon" /></div>
<h2 v-if="message" class="message">{{ message }}</h2>
</div>
<div class="sponsors">
<VPSponsors :data="data" />
</div>
<div v-if="actionLink" class="action">
<VPButton theme="sponsor" :text="actionText" :href="actionLink" />
</div>
</div>
</section>
</template>
<style scoped>
.VPHomeSponsors {
border-top: 1px solid var(--vp-c-gutter);
padding: 88px 24px 96px;
background-color: var(--vp-c-bg);
}
.container {
margin: 0 auto;
max-width: 1152px;
}
.love {
margin: 0 auto;
width: 28px;
height: 28px;
color: var(--vp-c-text-3);
}
.icon {
width: 28px;
height: 28px;
fill: currentColor;
}
.message {
margin: 0 auto;
padding-top: 10px;
max-width: 320px;
text-align: center;
line-height: 24px;
font-size: 16px;
font-weight: 500;
color: var(--vp-c-text-2);
}
.sponsors {
padding-top: 32px;
}
.action {
padding-top: 40px;
text-align: center;
}
</style>

View File

@@ -1,46 +0,0 @@
<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

@@ -1,33 +0,0 @@
<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))
</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

@@ -1,143 +0,0 @@
<script lang="ts" setup>
import { useWindowScroll } from '@vueuse/core'
import { onContentUpdated } from 'vitepress'
import { computed, onMounted, ref, shallowRef } from 'vue'
import { useData } from '../composables/data'
import { getHeaders, type MenuItem } from '../composables/outline'
import { useSidebar } from '../composables/sidebar'
import VPLocalNavOutlineDropdown from './VPLocalNavOutlineDropdown.vue'
import VPIconAlignLeft from './icons/VPIconAlignLeft.vue'
defineProps<{
open: boolean
}>()
defineEmits<{
(e: 'open-menu'): void
}>()
const { theme, frontmatter } = useData()
const { hasSidebar } = useSidebar()
// @ts-ignore
const { y } = useWindowScroll()
const headers = shallowRef<MenuItem[]>([])
const navHeight = ref(0)
onMounted(() => {
navHeight.value = parseInt(
getComputedStyle(document.documentElement).getPropertyValue(
'--vp-nav-height'
)
)
})
onContentUpdated(() => {
headers.value = getHeaders(frontmatter.value.outline ?? theme.value.outline)
})
const empty = computed(() => {
return headers.value.length === 0 && !hasSidebar.value
})
const classes = computed(() => {
return {
VPLocalNav: true,
fixed: empty.value,
'reached-top': y.value >= navHeight.value
}
})
</script>
<template>
<div
v-if="frontmatter.layout !== 'home' && (!empty || y >= navHeight)"
:class="classes"
>
<button
v-if="hasSidebar"
class="menu"
:aria-expanded="open"
aria-controls="VPSidebarNav"
@click="$emit('open-menu')"
>
<VPIconAlignLeft class="menu-icon" />
<span class="menu-text">
{{ theme.sidebarMenuLabel || 'Menu' }}
</span>
</button>
<VPLocalNavOutlineDropdown :headers="headers" :navHeight="navHeight" />
</div>
</template>
<style scoped>
.VPLocalNav {
position: sticky;
top: 0;
/*rtl:ignore*/
left: 0;
z-index: var(--vp-z-index-local-nav);
display: flex;
justify-content: space-between;
align-items: center;
border-top: 1px solid var(--vp-c-gutter);
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;
}
.VPLocalNav.reached-top {
border-top-color: transparent;
}
@media (min-width: 960px) {
.VPLocalNav {
display: none;
}
}
.menu {
display: flex;
align-items: center;
padding: 12px 24px 11px;
line-height: 24px;
font-size: 12px;
font-weight: 500;
color: var(--vp-c-text-2);
transition: color 0.5s;
}
.menu:hover {
color: var(--vp-c-text-1);
transition: color 0.25s;
}
@media (min-width: 768px) {
.menu {
padding: 0 32px;
}
}
.menu-icon {
margin-right: 8px;
width: 16px;
height: 16px;
fill: currentColor;
}
.VPOutlineDropdown {
padding: 12px 24px 11px;
}
@media (min-width: 768px) {
.VPOutlineDropdown {
padding: 12px 32px 11px;
}
}
</style>

View File

@@ -1,162 +0,0 @@
<script setup lang="ts">
import { onContentUpdated } from 'vitepress'
import { nextTick, ref } from 'vue'
import { useData } from '../composables/data'
import { resolveTitle, type MenuItem } from '../composables/outline'
import VPDocOutlineItem from './VPDocOutlineItem.vue'
import VPIconChevronRight from './icons/VPIconChevronRight.vue'
const props = defineProps<{
headers: MenuItem[]
navHeight: number
}>()
const { theme } = useData()
const open = ref(false)
const vh = ref(0)
const items = ref<HTMLDivElement>()
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' }">
<button @click="toggle" :class="{ open }" v-if="headers.length > 0">
{{ resolveTitle(theme) }}
<VPIconChevronRight class="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="headers" />
</div>
</div>
</Transition>
</div>
</template>
<style scoped>
.VPLocalNavOutlineDropdown {
padding: 12px 20px 11px;
}
.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;
width: 14px;
height: 14px;
fill: currentColor;
}
:deep(.outline-link) {
font-size: 14px;
padding: 2px 0;
}
.open > .icon {
transform: rotate(90deg);
}
.items {
position: absolute;
top: 64px;
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);
}
.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 .2s ease-out;
}
.flyout-leave-active {
transition: all .15s ease-in;
}
.flyout-enter-from,
.flyout-leave-to {
opacity: 0;
transform: translateY(-16px);
}
</style>

View File

@@ -1,931 +0,0 @@
<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 { inBrowser, useRouter, dataSymbol } 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 { useData } from '../composables/data'
import { createTranslate } from '../support/translation'
defineProps<{
placeholder: string
}>()
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)
)
})
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)
debouncedWatch(
() => [searchIndex.value, filterText.value, showDetailedList.value] as const,
async ([index, filterTextValue, showDetailedListValue], old, onCleanup) => {
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
const c = new Map<string, Map<string, string>>()
for (const { id, mod } of mods) {
const mapId = id.slice(0, id.indexOf('#'))
let map = c.get(mapId)
if (map) continue
map = new Map()
c.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 = c.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(false)
watch(results, (r) => {
selectedIndex.value = r.length ? 0 : -1
scrollToSelectedResult()
})
function scrollToSelectedResult() {
nextTick(() => {
const selectedEl = document.querySelector('.result.selected')
if (selectedEl) {
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.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 $t = createTranslate(theme.value.search?.options, 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) => {
return `(${term
.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')
.replace(/-/g, '\\x2d')})`
})
.join('|'),
'gi'
)
}
</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="placeholder"
id="localsearch-label"
for="localsearch-input"
>
<svg
class="search-icon"
width="18"
height="18"
viewBox="0 0 24 24"
aria-hidden="true"
>
<g
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
>
<circle cx="11" cy="11" r="8" />
<path d="m21 21l-4.35-4.35" />
</g>
</svg>
</label>
<div class="search-actions before">
<button
class="back-button"
:title="$t('modal.backButtonTitle')"
@click="$emit('close')"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 12H5m7 7l-7-7l7-7"
/>
</svg>
</button>
</div>
<input
ref="searchInput"
v-model="filterText"
:placeholder="placeholder"
id="localsearch-input"
aria-labelledby="localsearch-label"
class="search-input"
/>
<div class="search-actions">
<button
v-if="!disableDetailedView"
class="toggle-layout-button"
type="button"
:class="{ 'detailed-list': showDetailedList }"
:title="$t('modal.displayDetails')"
@click="
selectedIndex > -1 && (showDetailedList = !showDetailedList)
"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 14h7v7H3zM3 3h7v7H3zm11 1h7m-7 5h7m-7 6h7m-7 5h7"
/>
</svg>
</button>
<button
class="clear-button"
type="reset"
:disabled="disableReset"
:title="$t('modal.resetButtonTitle')"
@click="resetSearch"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M20 5H9l-7 7l7 7h11a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2Zm-2 4l-6 6m0-6l6 6"
/>
</svg>
</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="disableMouseOver = false"
>
<li
v-for="(p, index) in results"
:key="p.id"
role="option"
:aria-selected="selectedIndex === index ? 'true' : 'false'"
>
<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')"
>
<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" />
<svg width="18" height="18" viewBox="0 0 24 24">
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m9 18l6-6l-6-6"
/>
</svg>
</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"
>
{{ $t('modal.noResultsText') }} "<strong>{{ filterText }}</strong
>"
</li>
</ul>
<div class="search-keyboard-shortcuts">
<span>
<kbd :aria-label="$t('modal.footer.navigateUpKeyAriaLabel')">
<svg width="14" height="14" viewBox="0 0 24 24">
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 19V5m-7 7l7-7l7 7"
/>
</svg>
</kbd>
<kbd :aria-label="$t('modal.footer.navigateDownKeyAriaLabel')">
<svg width="14" height="14" viewBox="0 0 24 24">
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 5v14m7-7l-7 7l-7-7"
/>
</svg>
</kbd>
{{ $t('modal.footer.navigateText') }}
</span>
<span>
<kbd :aria-label="$t('modal.footer.selectKeyAriaLabel')">
<svg width="14" height="14" viewBox="0 0 24 24">
<g
fill="none"
stroke="currentcolor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
>
<path d="m9 10l-5 5l5 5" />
<path d="M20 4v7a4 4 0 0 1-4 4H4" />
</g>
</svg>
</kbd>
{{ $t('modal.footer.selectText') }}
</span>
<span>
<kbd :aria-label="$t('modal.footer.closeKeyAriaLabel')">esc</kbd>
{{ $t('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);
}
.search-icon {
margin: 8px;
}
@media (max-width: 767px) {
.search-icon {
display: none;
}
}
.search-input {
padding: 6px 12px;
font-size: inherit;
width: 100%;
}
@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: 75%;
pointer-events: none;
max-height: 140px;
overflow: hidden;
position: relative;
opacity: 0.5;
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

@@ -1,72 +0,0 @@
<script lang="ts" setup>
import VPMenuLink from './VPMenuLink.vue'
import VPMenuGroup from './VPMenuGroup.vue'
defineProps<{
items?: any[]
}>()
</script>
<template>
<div class="VPMenu">
<div v-if="items" class="items">
<template v-for="item in items" :key="item.text">
<VPMenuLink v-if="'link' in item" :item="item" />
<VPMenuGroup v-else :text="item.text" :items="item.items" />
</template>
</div>
<slot />
</div>
</template>
<style scoped>
.VPMenu {
border-radius: 12px;
padding: 12px;
min-width: 128px;
border: 1px solid var(--vp-c-divider);
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 .5s;
}
.VPMenu :deep(.action) {
padding-left: 24px;
}
</style>

View File

@@ -1,47 +0,0 @@
<script lang="ts" setup>
import VPMenuLink from './VPMenuLink.vue'
defineProps<{
text?: string
items: any[]
}>()
</script>
<template>
<div class="VPMenuGroup">
<p v-if="text" class="title">{{ text }}</p>
<template v-for="item in items">
<VPMenuLink v-if="'link' in item" :item="item" />
</template>
</div>
</template>
<style scoped>
.VPMenuGroup {
margin: 12px -12px 0;
border-top: 1px solid var(--vp-c-divider);
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

@@ -1,54 +0,0 @@
<script lang="ts" setup>
import type { DefaultTheme } from 'vitepress/theme'
import { useData } from '../composables/data'
import { isActive } from '../../shared'
import VPLink from './VPLink.vue'
defineProps<{
item: DefaultTheme.NavItemWithLink
}>()
const { page } = useData()
</script>
<template>
<div class="VPMenuLink">
<VPLink
:class="{ active: isActive(page.relativePath, item.activeMatch || item.link, !!item.activeMatch) }"
:href="item.link"
:target="item.target"
:rel="item.rel"
>
{{ item.text }}
</VPLink>
</div>
</template>
<style scoped>
.VPMenuGroup + .VPMenuLink {
margin: 12px -12px 0;
border-top: 1px solid var(--vp-c-divider);
padding: 12px 12px 0;
}
.link {
display: block;
border-radius: 6px;
padding: 0 12px;
line-height: 32px;
font-size: 14px;
font-weight: 500;
color: var(--vp-c-text-1);
white-space: nowrap;
transition: background-color 0.25s, color 0.25s;
}
.link:hover {
color: var(--vp-c-brand-1);
background-color: var(--vp-c-default-soft);
}
.link.active {
color: var(--vp-c-brand-1);
}
</style>

View File

@@ -1,57 +0,0 @@
<script setup lang="ts">
import { inBrowser } from 'vitepress'
import { computed, provide, watchEffect } from 'vue'
import { useData } from '../composables/data'
import { 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('close-screen', 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;
}
@media (min-width: 960px) {
.VPNav {
position: fixed;
}
}
</style>

View File

@@ -1,233 +0,0 @@
<script lang="ts" setup>
import { useWindowScroll } from '@vueuse/core'
import { ref, watchPostEffect } from 'vue'
import { useData } from '../composables/data'
import { useSidebar } from '../composables/sidebar'
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'
defineProps<{
isScreenOpen: boolean
}>()
defineEmits<{
(e: 'toggle-screen'): void
}>()
// @ts-ignore
const { y } = useWindowScroll()
const { hasSidebar } = useSidebar()
const { frontmatter } = useData()
const classes = ref<Record<string, boolean>>({})
watchPostEffect(() => {
classes.value = {
'has-sidebar': hasSidebar.value,
top: frontmatter.value.layout === 'home' && y.value === 0,
}
})
</script>
<template>
<div class="VPNavBar" :class="classes">
<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="curtain" />
<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>
</template>
<style scoped>
.VPNavBar {
position: relative;
border-bottom: 1px solid transparent;
padding: 0 8px 0 24px;
height: var(--vp-nav-height);
pointer-events: none;
white-space: nowrap;
}
@media (min-width: 768px) {
.VPNavBar {
padding: 0 32px;
}
}
@media (min-width: 960px) {
.VPNavBar.has-sidebar {
padding: 0;
}
.VPNavBar:not(.has-sidebar):not(.top) {
border-bottom-color: var(--vp-c-gutter);
background-color: var(--vp-nav-bg-color);
}
}
.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-right: 32px;
padding-left: var(--vp-sidebar-width);
}
}
@media (min-width: 1440px) {
.VPNavBar.has-sidebar .content {
padding-right: calc((100vw - var(--vp-layout-max-width)) / 2 + 32px);
padding-left: calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width));
}
}
.content-body {
display: flex;
justify-content: flex-end;
align-items: center;
height: calc(var(--vp-nav-height) - 1px);
transition: background-color 0.5s;
}
@media (min-width: 960px) {
.VPNavBar:not(.top) .content-body{
position: relative;
background-color: var(--vp-nav-bg-color);
}
}
@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;
}
@media (min-width: 960px) {
.VPNavBar.has-sidebar .curtain {
position: absolute;
right: 0;
bottom: -31px;
width: calc(100% - var(--vp-sidebar-width));
height: 32px;
}
.VPNavBar.has-sidebar .curtain::before {
display: block;
width: 100%;
height: 32px;
background: linear-gradient(var(--vp-c-bg), transparent 70%);
content: "";
}
}
@media (min-width: 1440px) {
.VPNavBar.has-sidebar .curtain {
width: calc(100% - ((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width)));
}
}
</style>

View File

@@ -1,25 +0,0 @@
<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'" class="VPNavBarAppearance">
<VPSwitchAppearance />
</div>
</template>
<style scoped>
.VPNavBarAppearance {
display: none;
}
@media (min-width: 1280px) {
.VPNavBarAppearance {
display: flex;
align-items: center;
}
}
</style>

View File

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

@@ -1,79 +0,0 @@
<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

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

View File

@@ -1,42 +0,0 @@
<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 ('link' in navItem) {
return isActive(
page.value.relativePath,
navItem.link,
!!props.item.activeMatch
)
} else {
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

@@ -1,52 +0,0 @@
<script lang="ts" setup>
import type { DefaultTheme } from 'vitepress/theme'
import { useData } from '../composables/data'
import { isActive } from '../../shared'
import VPLink from './VPLink.vue'
defineProps<{
item: DefaultTheme.NavItemWithLink
}>()
const { page } = useData()
</script>
<template>
<VPLink
:class="{
VPNavBarMenuLink: true,
active: isActive(
page.relativePath,
item.activeMatch || item.link,
!!item.activeMatch
)
}"
:href="item.link"
:target="item.target"
:rel="item.rel"
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

@@ -1,209 +0,0 @@
<script lang="ts" setup>
import '@docsearch/css'
import { onKeyStroke } from '@vueuse/core'
import {
computed,
defineAsyncComponent,
onMounted,
onUnmounted,
ref
} from 'vue'
import type { DefaultTheme } from '../../shared'
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, localeIndex } = 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 buttonText = computed(() => {
const options = theme.value.search?.options ?? theme.value.algolia
return (
options?.locales?.[localeIndex.value]?.translations?.button?.buttonText ||
options?.translations?.button?.buttonText ||
'Search'
)
})
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"
:placeholder="buttonText"
@close="showSearch = false"
/>
<div id="local-search">
<VPNavBarSearchButton
:placeholder="buttonText"
@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 :placeholder="buttonText" @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;
}
}
.dark .DocSearch-Footer {
border-top: 1px solid var(--vp-c-divider);
}
.DocSearch-Form {
border: 1px solid var(--vp-c-brand-1);
background-color: var(--vp-c-white);
}
.dark .DocSearch-Form {
background-color: var(--vp-c-default-soft);
}
.DocSearch-Screen-Icon > svg {
margin: auto;
}
</style>

View File

@@ -1,210 +0,0 @@
<script lang="ts" setup>
defineProps<{
placeholder: string
}>()
</script>
<template>
<button type="button" class="DocSearch DocSearch-Button" aria-label="Search">
<span class="DocSearch-Button-Container">
<svg
class="DocSearch-Search-Icon"
width="20"
height="20"
viewBox="0 0 20 20"
aria-label="search icon"
>
<path
d="M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z"
stroke="currentColor"
fill="none"
fill-rule="evenodd"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
<span class="DocSearch-Button-Placeholder">{{ placeholder }}</span>
</span>
<span class="DocSearch-Button-Keys">
<kbd class="DocSearch-Button-Key"></kbd>
<kbd class="DocSearch-Button-Key">K</kbd>
</span>
</button>
</template>
<style>
[class*='DocSearch'] {
--docsearch-primary-color: var(--vp-c-brand-1);
--docsearch-highlight-color: var(--docsearch-primary-color);
--docsearch-text-color: var(--vp-c-text-1);
--docsearch-muted-color: var(--vp-c-text-2);
--docsearch-searchbox-shadow: none;
--docsearch-searchbox-background: transparent;
--docsearch-searchbox-focus-background: transparent;
--docsearch-key-gradient: transparent;
--docsearch-key-shadow: none;
--docsearch-modal-background: var(--vp-c-bg-soft);
--docsearch-footer-background: var(--vp-c-bg);
}
.dark [class*='DocSearch'] {
--docsearch-modal-shadow: none;
--docsearch-footer-shadow: none;
--docsearch-logo-color: var(--vp-c-text-2);
--docsearch-hit-background: var(--vp-c-default-soft);
--docsearch-hit-color: var(--vp-c-text-2);
--docsearch-hit-shadow: none;
}
.DocSearch-Button {
display: flex;
justify-content: center;
align-items: center;
margin: 0;
padding: 0;
width: 48px;
height: 55px;
background: transparent;
transition: border-color 0.25s;
}
.DocSearch-Button:hover {
background: transparent;
}
.DocSearch-Button:focus {
outline: 1px dotted;
outline: 5px auto -webkit-focus-ring-color;
}
.DocSearch-Button:focus:not(:focus-visible) {
outline: none !important;
}
@media (min-width: 768px) {
.DocSearch-Button {
justify-content: flex-start;
border: 1px solid transparent;
border-radius: 8px;
padding: 0 10px 0 12px;
width: 100%;
height: 40px;
background-color: var(--vp-c-bg-alt);
}
.DocSearch-Button:hover {
border-color: var(--vp-c-brand-1);
background: var(--vp-c-bg-alt);
}
}
.DocSearch-Button .DocSearch-Button-Container {
display: flex;
align-items: center;
}
.DocSearch-Button .DocSearch-Search-Icon {
position: relative;
width: 16px;
height: 16px;
color: var(--vp-c-text-1);
fill: currentColor;
transition: color 0.5s;
}
.DocSearch-Button:hover .DocSearch-Search-Icon {
color: var(--vp-c-text-1);
}
@media (min-width: 768px) {
.DocSearch-Button .DocSearch-Search-Icon {
top: 1px;
margin-right: 8px;
width: 14px;
height: 14px;
color: var(--vp-c-text-2);
}
}
.DocSearch-Button .DocSearch-Button-Placeholder {
display: none;
margin-top: 2px;
padding: 0 16px 0 0;
font-size: 13px;
font-weight: 500;
color: var(--vp-c-text-2);
transition: color 0.5s;
}
.DocSearch-Button:hover .DocSearch-Button-Placeholder {
color: var(--vp-c-text-1);
}
@media (min-width: 768px) {
.DocSearch-Button .DocSearch-Button-Placeholder {
display: inline-block;
}
}
.DocSearch-Button .DocSearch-Button-Keys {
/*rtl:ignore*/
direction: ltr;
display: none;
min-width: auto;
}
@media (min-width: 768px) {
.DocSearch-Button .DocSearch-Button-Keys {
display: flex;
align-items: center;
}
}
.DocSearch-Button .DocSearch-Button-Key {
display: block;
margin: 2px 0 0 0;
border: 1px solid var(--vp-c-divider);
/*rtl:begin:ignore*/
border-right: none;
border-radius: 4px 0 0 4px;
padding-left: 6px;
/*rtl:end:ignore*/
min-width: 0;
width: auto;
height: 22px;
line-height: 22px;
font-family: var(--vp-font-family-base);
font-size: 12px;
font-weight: 500;
transition: color 0.5s, border-color 0.5s;
}
.DocSearch-Button .DocSearch-Button-Key + .DocSearch-Button-Key {
/*rtl:begin:ignore*/
border-right: 1px solid var(--vp-c-divider);
border-left: none;
border-radius: 0 4px 4px 0;
padding-left: 2px;
padding-right: 6px;
/*rtl:end:ignore*/
}
.DocSearch-Button .DocSearch-Button-Key:first-child {
font-size: 0 !important;
}
.DocSearch-Button .DocSearch-Button-Key:first-child:after {
content: 'Ctrl';
font-size: 12px;
letter-spacing: normal;
color: var(--docsearch-muted-color);
}
.mac .DocSearch-Button .DocSearch-Button-Key:first-child:after {
content: '\2318';
}
.DocSearch-Button .DocSearch-Button-Key:first-child > * {
display: none;
}
</style>

View File

@@ -1,27 +0,0 @@
<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

@@ -1,52 +0,0 @@
<script setup lang="ts">
import { useData } from '../composables/data'
import { useLangs } from '../composables/langs'
import { useSidebar } from '../composables/sidebar'
import { normalizeLink } from '../support/utils'
import VPImage from './VPImage.vue'
const { site, theme } = useData()
const { hasSidebar } = useSidebar()
const { currentLang } = useLangs()
</script>
<template>
<div class="VPNavBarTitle" :class="{ 'has-sidebar': hasSidebar }">
<a class="title" :href="theme.logoLink ?? normalizeLink(currentLang.link)">
<slot name="nav-bar-title-before" />
<VPImage v-if="theme.logo" class="logo" :image="theme.logo" />
<template v-if="theme.siteTitle">{{ theme.siteTitle }}</template>
<template v-else-if="theme.siteTitle === undefined">{{ site.title }}</template>
<slot name="nav-bar-title-after" />
</a>
</div>
</template>
<style scoped>
.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

@@ -1,48 +0,0 @@
<script lang="ts" setup>
import VPIconLanguages from './icons/VPIconLanguages.vue'
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="VPIconLanguages"
:label="theme.langMenuLabel || 'Change language'"
>
<div class="items">
<p class="title">{{ currentLang.label }}</p>
<template v-for="locale in localeLinks" :key="locale.link">
<VPMenuLink :item="locale" />
</template>
</div>
</VPFlyout>
</template>
<style scoped>
.VPNavBarTranslations {
display: none;
}
@media (min-width: 1280px) {
.VPNavBarTranslations {
display: flex;
align-items: center;
}
}
.title {
padding: 0 24px 0 12px;
line-height: 32px;
font-size: 14px;
font-weight: 700;
color: var(--vp-c-text-1);
}
</style>

View File

@@ -1,99 +0,0 @@
<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) + 1px);
/*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.5s;
pointer-events: auto;
}
.VPNavScreen.fade-enter-active,
.VPNavScreen.fade-leave-active {
transition: opacity 0.25s;
}
.VPNavScreen.fade-enter-active .container,
.VPNavScreen.fade-leave-active .container {
transition: transform 0.25s ease;
}
.VPNavScreen.fade-enter-from,
.VPNavScreen.fade-leave-to {
opacity: 0;
}
.VPNavScreen.fade-enter-from .container,
.VPNavScreen.fade-leave-to .container {
transform: translateY(-8px);
}
@media (min-width: 768px) {
.VPNavScreen {
display: none;
}
}
.container {
margin: 0 auto;
padding: 24px 0 96px;
max-width: 288px;
}
.menu + .translations,
.menu + .appearance,
.translations + .appearance {
margin-top: 24px;
}
.menu + .social-links {
margin-top: 16px;
}
.appearance + .social-links {
margin-top: 16px;
}
</style>

View File

@@ -1,33 +0,0 @@
<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" 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

@@ -1,23 +0,0 @@
<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="item.text">
<VPNavScreenMenuLink
v-if="'link' in item"
:item="item"
/>
<VPNavScreenMenuGroup
v-else
:text="item.text || ''"
:items="item.items"
/>
</template>
</nav>
</template>

View File

@@ -1,115 +0,0 @@
<script lang="ts" setup>
import { computed, ref } from 'vue'
import VPIconPlus from './icons/VPIconPlus.vue'
import VPNavScreenMenuGroupLink from './VPNavScreenMenuGroupLink.vue'
import VPNavScreenMenuGroupSection from './VPNavScreenMenuGroupSection.vue'
const props = defineProps<{
text: string
items: any[]
}>()
const isOpen = ref(false)
const groupId = computed(() =>
`NavScreenGroup-${props.text.replace(' ', '-').toLowerCase()}`
)
function toggle() {
isOpen.value = !isOpen.value
}
</script>
<template>
<div class="VPNavScreenMenuGroup" :class="{ open: isOpen }">
<button
class="button"
:aria-controls="groupId"
:aria-expanded="isOpen"
@click="toggle"
>
<span class="button-text">{{ text }}</span>
<VPIconPlus class="button-icon" />
</button>
<div :id="groupId" class="items">
<template v-for="item in items" :key="item.text">
<div v-if="'link' in item" :key="item.text" class="item">
<VPNavScreenMenuGroupLink :item="item" />
</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 {
width: 14px;
height: 14px;
fill: var(--vp-c-text-2);
transition: fill 0.5s, transform 0.25s;
}
.group:first-child {
padding-top: 0px;
}
.group + .group,
.group + .item {
padding-top: 4px;
}
</style>

View File

@@ -1,39 +0,0 @@
<script lang="ts" setup>
import type { DefaultTheme } from 'vitepress/theme'
import { inject } from 'vue'
import VPLink from './VPLink.vue'
defineProps<{
item: DefaultTheme.NavItemWithLink
}>()
const closeScreen = inject('close-screen') as () => void
</script>
<template>
<VPLink
class="VPNavScreenMenuGroupLink"
:href="item.link"
:target="item.target"
:rel="item.rel"
@click="closeScreen"
>
{{ item.text }}
</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

@@ -1,34 +0,0 @@
<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="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

@@ -1,40 +0,0 @@
<script lang="ts" setup>
import type { DefaultTheme } from 'vitepress/theme'
import { inject } from 'vue'
import VPLink from './VPLink.vue'
defineProps<{
item: DefaultTheme.NavItemWithLink
}>()
const closeScreen = inject('close-screen') as () => void
</script>
<template>
<VPLink
class="VPNavScreenMenuLink"
:href="item.link"
:target="item.target"
:rel="item.rel"
@click="closeScreen"
>
{{ item.text }}
</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

@@ -1,14 +0,0 @@
<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

@@ -1,77 +0,0 @@
<script setup lang="ts">
import { ref } from 'vue'
import VPIconChevronDown from './icons/VPIconChevronDown.vue'
import VPIconLanguages from './icons/VPIconLanguages.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">
<VPIconLanguages class="icon lang" />
{{ currentLang.label }}
<VPIconChevronDown class="icon chevron" />
</button>
<ul class="list">
<li v-for="locale in localeLinks" :key="locale.link" class="item">
<VPLink class="link" :href="locale.link">{{ 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 {
width: 16px;
height: 16px;
fill: currentColor;
}
.icon.lang {
margin-right: 8px;
}
.icon.chevron {
margin-left: 4px;
}
.list {
padding: 4px 0 0 24px;
}
.link {
line-height: 32px;
font-size: 13px;
color: var(--vp-c-text-1);
}
</style>

View File

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

View File

@@ -1,139 +0,0 @@
<script lang="ts" setup>
import { useScrollLock } from '@vueuse/core'
import { inBrowser } from 'vitepress'
import { ref, watch } from 'vue'
import { useSidebar } from '../composables/sidebar'
import VPSidebarItem from './VPSidebarItem.vue'
const { sidebarGroups, hasSidebar } = useSidebar()
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' }
)
</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" />
<div v-for="item in sidebarGroups" :key="item.text" class="group">
<VPSidebarItem :item="item" :depth="0" />
</div>
<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 {
z-index: 1;
padding-top: var(--vp-nav-height);
padding-bottom: 128px;
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: -64px;
left: 0;
z-index: 1;
margin-top: calc(var(--vp-nav-height) * -1);
margin-right: -32px;
margin-left: -32px;
height: var(--vp-nav-height);
background-color: var(--vp-sidebar-bg-color);
}
}
.nav {
outline: 0;
}
.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

@@ -1,253 +0,0 @@
<script setup lang="ts">
import { computed } from 'vue'
import type { DefaultTheme } from 'vitepress/theme'
import { useSidebarControl } from '../composables/sidebar'
import VPIconChevronRight from './icons/VPIconChevronRight.vue'
import VPLink from './VPLink.vue'
const props = defineProps<{
item: DefaultTheme.SidebarItem
depth: number
}>()
const {
collapsed,
collapsible,
isLink,
isActiveLink,
hasActiveLink,
hasChildren,
toggle
} = useSidebarControl(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"
class="caret"
role="button"
aria-label="toggle section"
@click="onCaretClick"
@keydown.enter="onCaretClick"
tabindex="0"
>
<VPIconChevronRight class="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 {
width: 18px;
height: 18px;
fill: currentColor;
transform: rotate(90deg);
transition: transform 0.25s;
}
.VPSidebarItem.collapsed .caret-icon {
transform: rotate(0);
}
.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

@@ -1,68 +0,0 @@
<script lang="ts" setup>
import { ref, watch } from 'vue'
import { useRoute } from 'vitepress'
const route = useRoute()
const backToTop = ref()
watch(() => route.path, () => backToTop.value.focus())
function focusOnTargetAnchor({ target }: Event) {
const el = document.getElementById(
decodeURIComponent((target as HTMLAnchorElement).hash).slice(1)
)
if (el) {
const removeTabIndex = () => {
el.removeAttribute('tabindex')
el.removeEventListener('blur', removeTabIndex)
}
el.setAttribute('tabindex', '-1')
el.addEventListener('blur', removeTabIndex)
el.focus()
window.scrollTo(0, 0)
}
}
</script>
<template>
<span ref="backToTop" tabindex="-1" />
<a
href="#VPContent"
class="VPSkipLink visually-hidden"
@click="focusOnTargetAnchor"
>
Skip to content
</a>
</template>
<style scoped>
.VPSkipLink {
top: 8px;
left: 8px;
padding: 8px 16px;
z-index: 999;
border-radius: 8px;
font-size: 12px;
font-weight: bold;
text-decoration: none;
color: var(--vp-c-brand-1);
box-shadow: var(--vp-shadow-3);
background-color: var(--vp-c-bg);
}
.VPSkipLink:focus {
height: auto;
width: auto;
clip: auto;
clip-path: none;
}
@media (min-width: 1280px) {
.VPSkipLink {
top: 14px;
left: 16px;
}
}
</style>

View File

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

View File

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

View File

@@ -1,48 +0,0 @@
<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

@@ -1,48 +0,0 @@
<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">
<h4 class="visually-hidden">{{ sponsor.name }}</h4>
<img
class="vp-sponsor-grid-image"
:src="sponsor.img"
:alt="sponsor.name"
/>
</article>
</a>
</div>
</div>
</template>

View File

@@ -1,63 +0,0 @@
<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(svg) {
position: absolute;
top: 3px;
left: 3px;
width: 12px;
height: 12px;
fill: var(--vp-c-text-2);
}
.dark .icon :deep(svg) {
fill: var(--vp-c-text-1);
transition: opacity 0.25s !important;
}
</style>

View File

@@ -1,48 +0,0 @@
<script lang="ts" setup>
import { inject } from 'vue'
import { useData } from '../composables/data'
import VPSwitch from './VPSwitch.vue'
import VPIconMoon from './icons/VPIconMoon.vue'
import VPIconSun from './icons/VPIconSun.vue'
const { isDark } = useData()
const toggleAppearance = inject('toggle-appearance', () => {
isDark.value = !isDark.value
})
</script>
<template>
<VPSwitch
title="toggle dark mode"
class="VPSwitchAppearance"
:aria-checked="isDark"
@click="toggleAppearance"
>
<VPIconSun class="sun" />
<VPIconMoon class="moon" />
</VPSwitch>
</template>
<style scoped>
.sun {
opacity: 1;
}
.moon {
opacity: 0;
}
.dark .sun {
opacity: 0;
}
.dark .moon {
opacity: 1;
}
.dark .VPSwitchAppearance :deep(.check) {
/*rtl:ignore*/
transform: translateX(18px);
}
</style>

View File

@@ -1,66 +0,0 @@
<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="size" :member="member" />
</div>
</div>
</div>
</template>
<style scoped>
.VPTeamMembers.small .container {
grid-template-columns: repeat(auto-fit, minmax(224px, 1fr));
}
.VPTeamMembers.small.count-1 .container {
max-width: 276px;
}
.VPTeamMembers.small.count-2 .container {
max-width: calc(276px * 2 + 24px);
}
.VPTeamMembers.small.count-3 .container {
max-width: calc(276px * 3 + 24px * 2);
}
.VPTeamMembers.medium .container {
grid-template-columns: repeat(auto-fit, minmax(256px, 1fr));
}
@media (min-width: 375px) {
.VPTeamMembers.medium .container {
grid-template-columns: repeat(auto-fit, minmax(288px, 1fr));
}
}
.VPTeamMembers.medium.count-1 .container {
max-width: 368px;
}
.VPTeamMembers.medium.count-2 .container {
max-width: calc(368px * 2 + 24px);
}
.container {
display: grid;
gap: 24px;
margin: 0 auto;
max-width: 1152px;
}
</style>

View File

@@ -1,228 +0,0 @@
<script setup lang="ts">
import type { DefaultTheme } from 'vitepress/theme'
import VPIconHeart from './icons/VPIconHeart.vue'
import VPLink from './VPLink.vue'
import VPSocialLinks from './VPSocialLinks.vue'
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" />
</div>
</div>
</div>
<div v-if="member.sponsor" class="sp">
<VPLink class="sp-link" :href="member.sponsor" no-icon>
<VPIconHeart class="sp-icon" /> Sponsor
</VPLink>
</div>
</article>
</template>
<style scoped>
.VPTeamMembersItem {
display: flex;
flex-direction: column;
gap: 2px;
border-radius: 12px;
width: 100%;
height: 100%;
overflow: hidden;
}
.VPTeamMembersItem.small .profile {
padding: 32px;
}
.VPTeamMembersItem.small .data {
padding-top: 20px;
}
.VPTeamMembersItem.small .avatar {
width: 64px;
height: 64px;
}
.VPTeamMembersItem.small .name {
line-height: 24px;
font-size: 16px;
}
.VPTeamMembersItem.small .affiliation {
padding-top: 4px;
line-height: 20px;
font-size: 14px;
}
.VPTeamMembersItem.small .desc {
padding-top: 12px;
line-height: 20px;
font-size: 14px;
}
.VPTeamMembersItem.small .links {
margin: 0 -16px -20px;
padding: 10px 0 0;
}
.VPTeamMembersItem.medium .profile {
padding: 48px 32px;
}
.VPTeamMembersItem.medium .data {
padding-top: 24px;
text-align: center;
}
.VPTeamMembersItem.medium .avatar {
width: 96px;
height: 96px;
}
.VPTeamMembersItem.medium .name {
letter-spacing: 0.15px;
line-height: 28px;
font-size: 20px;
}
.VPTeamMembersItem.medium .affiliation {
padding-top: 4px;
font-size: 16px;
}
.VPTeamMembersItem.medium .desc {
padding-top: 16px;
max-width: 288px;
font-size: 16px;
}
.VPTeamMembersItem.medium .links {
margin: 0 -16px -12px;
padding: 16px 12px 0;
}
.profile {
flex-grow: 1;
background-color: var(--vp-c-bg-soft);
}
.data {
text-align: center;
}
.avatar {
position: relative;
flex-shrink: 0;
margin: 0 auto;
border-radius: 50%;
box-shadow: var(--vp-shadow-3);
}
.avatar-img {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
border-radius: 50%;
object-fit: cover;
}
.name {
margin: 0;
font-weight: 600;
}
.affiliation {
margin: 0;
font-weight: 500;
color: var(--vp-c-text-2);
}
.org.link {
color: var(--vp-c-text-2);
transition: color 0.25s;
}
.org.link:hover {
color: var(--vp-c-brand-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;
width: 16px;
height: 16px;
fill: currentColor;
}
</style>

View File

@@ -1,53 +0,0 @@
<template>
<div class="VPTeamPage">
<slot />
</div>
</template>
<style scoped>
.VPTeamPage {
padding-bottom: 96px;
}
@media (min-width: 768px) {
.VPTeamPage {
padding-bottom: 128px;
}
}
:slotted(.VPTeamPageSection + .VPTeamPageSection),
:slotted(.VPTeamMembers + .VPTeamPageSection) {
margin-top: 64px;
}
:slotted(.VPTeamMembers + .VPTeamMembers) {
margin-top: 24px;
}
@media (min-width: 768px) {
:slotted(.VPTeamPageTitle + .VPTeamPageSection) {
margin-top: 16px;
}
:slotted(.VPTeamPageSection + .VPTeamPageSection),
:slotted(.VPTeamMembers + .VPTeamPageSection) {
margin-top: 96px;
}
}
:slotted(.VPTeamMembers) {
padding: 0 24px;
}
@media (min-width: 768px) {
:slotted(.VPTeamMembers) {
padding: 0 48px;
}
}
@media (min-width: 960px) {
:slotted(.VPTeamMembers) {
padding: 0 64px;
}
}
</style>

View File

@@ -1,77 +0,0 @@
<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

@@ -1,63 +0,0 @@
<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

@@ -1,8 +0,0 @@
<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

@@ -1,8 +0,0 @@
<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

@@ -1,8 +0,0 @@
<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

@@ -1,7 +0,0 @@
<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

@@ -1,7 +0,0 @@
<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

@@ -1,5 +0,0 @@
<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

@@ -1,5 +0,0 @@
<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

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

View File

@@ -1,5 +0,0 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewBox="0 0 24 24">
<path d="M18,16c-0.3,0-0.5-0.1-0.7-0.3L12,10.4l-5.3,5.3c-0.4,0.4-1,0.4-1.4,0s-0.4-1,0-1.4l6-6c0.4-0.4,1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4C18.5,15.9,18.3,16,18,16z" />
</svg>
</template>

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