Initalize

This commit is contained in:
Your Name
2026-05-03 12:12:57 -04:00
commit 38652eb9b5
10603 changed files with 1762136 additions and 0 deletions

View File

@@ -0,0 +1,138 @@
<template>
<div
v-if="showAlert && props.content !== ''"
:class="`alert-banner alert-${props.type}`"
>
<div class="alert-content">
<button
v-if="props.closeable"
aria-label="Close"
class="alert-dismiss-button"
@click="dismissAlert"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-6 h-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M6 18 18 6M6 6l12 12"
/>
</svg>
</button>
<div
class="content"
v-html="props.content"
/>
</div>
</div>
</template>
<script setup>
import {onMounted, onUnmounted, ref, watch} from 'vue';
const props = defineProps({
content: {
type: String,
default: '',
required: true,
},
type: {
type: String,
default: 'tip',
},
closeable: {
type: Boolean,
default: true,
},
});
const isAlertMode = ref(props.content !== '');
const showAlert = ref(isAlertMode.value);
const update = (value = isAlertMode.value) => {
const htmlEl = window.document.querySelector('html');
htmlEl.classList.toggle('alert', value);
showAlert.value = value;
};
const dismissAlert = () => update(false);
onMounted(() => watch(isAlertMode, update, {immediate: true}));
onUnmounted(() => update(false));
</script>
<style lang="scss" scoped>
:root {
--vpl-alert-height: 40px;
}
.alert-banner {
position: fixed;
top: 0;
left: 0;
right: 0;
height: var(--vpl-alert-height);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
.alert-content {
display: flex;
justify-content: center;
align-items: center;
gap: 1px;
.content {
font-size: 0.95rem;
font-weight: 500;
}
}
.alert-dismiss-button {
font-size: 1rem;
cursor: pointer;
background-color: transparent;
border: 0;
font-weight: 700;
width: 13px;
}
&.alert-brand {
background-color: var(--vp-c-brand-soft-hex);
color: var(--vp-c-brand-hard);
}
&.alert-danger {
background-color: var(--vp-c-red-soft-hex);
color: var(--vp-c-red-hard);
}
&.alert-tip {
background-color: var(--vp-c-indigo-soft-hex);
color: var(--vp-c-indigo-hard);
}
&.alert-info {
background-color: var(--vp-c-gray-soft-hex);
color: var(--vp-c-gray-hard);
}
&.alert-success {
background-color: var(--vp-c-green-soft-hex);
color: var(--vp-c-green-hard);
}
&.alert-warning {
background-color: var(--vp-c-yellow-soft-hex);
color: var(--vp-c-yellow-hard);
}
}
</style>

View File

@@ -0,0 +1,86 @@
<template>
<div id="docsearch" />
</template>
<script setup>
import docsearch from '@docsearch/js';
import {useData, useRoute, useRouter} from 'vitepress';
import {nextTick, onMounted, watch} from 'vue';
const props = defineProps({
algolia: {
type: Object,
default: () => ({}),
},
});
const router = useRouter();
const route = useRoute();
const {site, localeIndex, lang} = useData();
onMounted(update);
watch(localeIndex, update);
async function update() {
await nextTick();
const options = {
...props.algolia,
...props.algolia.locales?.[localeIndex.value],
};
const rawFacetFilters = options.searchParameters?.facetFilters ?? [];
const facetFilters = [
...(Array.isArray(rawFacetFilters)
? rawFacetFilters
: [rawFacetFilters]
).filter(f => !f.startsWith('lang:')),
`lang:${lang.value}`,
];
initialize({
...options,
searchParameters: {
...options.searchParameters,
facetFilters,
},
});
}
function initialize(userOptions = {}) {
const options = Object.assign({}, 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 => Object.assign({}, item, {url: getRelativePath(item.url)}));
},
hitComponent({hit, children}) {
return {
__v: null,
type: 'a',
ref: undefined,
constructor: undefined,
target: '_blank',
key: undefined,
props: {href: hit.url, children, target: '_self'},
};
},
});
docsearch(options);
}
function getRelativePath(url) {
const {pathname, hash} = new URL(url, location.origin);
return pathname.replace(/\.html$/, site.value.cleanUrls ? '' : '.html') + hash;
}
</script>

View File

@@ -0,0 +1,59 @@
<template>
<Page>
<template #doc-top>
<img src="https://i.pinimg.com/originals/4c/d9/ce/4cd9ce636c6d5f23688f0fda99cd81cf.gif">
</template>
<template #doc-before>
<img src="https://i.pinimg.com/originals/4c/d9/ce/4cd9ce636c6d5f23688f0fda99cd81cf.gif">
</template>
<template #doc-after>
<img src="https://i.pinimg.com/originals/4c/d9/ce/4cd9ce636c6d5f23688f0fda99cd81cf.gif">
</template>
<template #doc-bottom>
<img src="https://i.pinimg.com/originals/4c/d9/ce/4cd9ce636c6d5f23688f0fda99cd81cf.gif">
</template>
<template #aside-top>
<img src="https://i.pinimg.com/originals/4c/d9/ce/4cd9ce636c6d5f23688f0fda99cd81cf.gif">
</template>
<template #aside-outline-before>
<img src="https://i.pinimg.com/originals/4c/d9/ce/4cd9ce636c6d5f23688f0fda99cd81cf.gif">
</template>
<template #aside-outline-after>
<img src="https://i.pinimg.com/originals/4c/d9/ce/4cd9ce636c6d5f23688f0fda99cd81cf.gif">
</template>
<template #aside-ads-before>
<img src="https://i.pinimg.com/originals/4c/d9/ce/4cd9ce636c6d5f23688f0fda99cd81cf.gif">
</template>
<template #aside-ads-after>
<img src="https://i.pinimg.com/originals/4c/d9/ce/4cd9ce636c6d5f23688f0fda99cd81cf.gif">
</template>
<template #aside-bottom>
<img src="https://i.pinimg.com/originals/4c/d9/ce/4cd9ce636c6d5f23688f0fda99cd81cf.gif">
</template>
<div class="cat-container">
<Content />
</div>
</Page>
</template>
<script setup>
import Page from 'vitepress/dist/client/theme-default/components/VPDoc.vue';
</script>
<style lang="scss" scoped>
.contributors {
float: left;
max-width: 70%;
overflow: hidden;
max-height: 70px;
}
.contributors-flex {
height: 65px;
display: flex;
flex-direction: column;
flex-wrap: wrap;
justify-content: flex-end;
}
</style>

View File

@@ -0,0 +1,96 @@
<template>
<div class="collection-header">
<div class="collection-type">
<Icon
v-if="collection !== false"
:icon="icon"
:link="iconLink"
:title="collection"
/>
</div>
<div
v-if="authors.length > 0"
class="collection-avatars"
>
<div class="label">
By
<Link
v-for="(author, index) in authors"
:key="author.name"
:href="author.link"
no-icon
>
<span class="underline">{{ author.name }}</span><span class="separator">{{ getSeparator(index, authors.length) }}</span>
</Link>
</div>
<Author
v-for="author in authors"
:key="author.name"
size="icon"
:member="author"
/>
</div>
</div>
</template>
<script setup>
import {computed} from 'vue';
import {useData} from 'vitepress';
import Author from './VPLTeamMembersItem.vue';
import Icon from './VPLCollectionIcon.vue';
import Link from './VPLLink.vue';
const {frontmatter, page} = useData();
const authors = computed(() => frontmatter.value?.authors ?? []);
const collection = computed(() => frontmatter.value?.collection ?? false);
const icon = computed(() => page.value?.collection?.icon ?? false);
const iconLink = computed(() => page.value?.collection?.iconLink ?? false);
const getSeparator = (index, end = 0) => {
return index + 1 === end ? '' : ', ';
};
</script>
<style lang="scss" scoped>
.collection-header {
margin-bottom: 24px;
font-size: .75em;
align-items: flex-start;
z-index: 1;
position: relative;
display: flex;
justify-content: space-between;
a {
font-weight: 500;
color: color-mix(in srgb, var(--vp-c-brand-1) 90%, white);
.underline {
text-underline-offset: 2px;
text-decoration: underline;
}
.separator {
color: var(--vp-c-text-3);
text-decoration: none;
}
}
.collection-type {
display: flex;
gap: 4px;
align-items: center;
color: var(--vp-c-text-2);
}
.collection-avatars {
display: flex;
justify-content: flex-end;
.label {
margin-right: 14px;
}
.VPTeamMembersItem.icon {
overflow: visible;
margin-left: -14px;
}
}
}
</style>

View File

@@ -0,0 +1,59 @@
<template>
<div class="collection-icon">
<Link
:href="link"
rel="noopener"
>
<span
class="icon"
v-html="icon"
/>{{ title }}
</Link>
</div>
</template>
<script setup>
import Link from './VPLLink.vue';
const {title, icon, link} = defineProps({
icon: {
type: String,
default: () => {
return '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" /></svg>';
},
},
link: {
type: String,
default: undefined,
},
title: {
type: String,
default: 'doc',
},
});
</script>
<style lang="scss" scoped>
.collection-icon {
gap: 2px;
display: flex;
justify-content: flex-end;
align-items: center;
text-align: center;
font-weight: 500;
text-transform: capitalize;
color: var(--vp-c-text-3);
a.VPLink.link {
display: flex;
align-items: center;
gap: 2px;
}
&:hover {
color: var(--vp-c-brand-1);
}
.icon {
width: 16px;
}
}
</style>

View File

@@ -0,0 +1,188 @@
<template>
<article
class="collection-article-card"
:class="`${page.type} ${size}`"
>
<Icon
v-if="icon !== false"
:icon="icon"
:link="iconLink"
:title="page.type"
/>
<Link :href="page.url">
<h2 class="title">
{{ page.title }}
</h2>
</Link>
<div
class="summary"
>
{{ page.summary }}
</div>
<div class="attribution">
<div class="authors">
<div
v-if="page.authors && page.authors.length > 0"
class="avatars"
>
<Author
v-for="author in page.authors"
:key="author.name"
size="icon"
:member="author"
/>
</div>
<Link
v-for="(author, index) in page.authors"
:key="author.name"
class="names"
:href="author.link"
no-icon
>
<span class="underline">{{ author.name }}</span><span class="separator">{{ getSeparator(index, page.authors.length) }}&nbsp;</span>
</Link>
</div>
<time
v-if="more === 'date'"
class="date"
:datetime="page.datetime"
>
{{ hdate }}
</time>
<Link
v-else
:href="page.url"
class="read-more"
>
<time :datetime="page.datetime" />
Read More ->
</Link>
</div>
</article>
</template>
<script setup>
import {computed} from 'vue';
import {useData} from 'vitepress';
import Link from './VPLLink.vue';
import Author from './VPLTeamMembersItem.vue';
import Icon from './VPLCollectionIcon.vue';
const {more, page, size} = defineProps({
page: {
type: Object,
required: true,
},
size: {
type: String,
default: 'medium',
},
more: {
type: String,
default: 'readmore',
},
});
const {theme} = useData();
const collection = computed(() => page?.collection ?? false);
const icon = computed(() => theme.value?.collections[collection.value]?.icon ?? false);
const iconLink = computed(() => theme.value?.collections[collection.value]?.iconLink ?? false);
const hdate = computed(() => {
return new Date(page.date ?? page.timestamp).toLocaleDateString(undefined, {
year: 'numeric',
month: 'long',
day: 'numeric',
});
});
const getSeparator = (index, end = 0) => {
return index + 1 === end ? '' : ',';
};
</script>
<style lang="scss" scoped>
.collection-article-card {
background-color: var(--vp-c-bg-soft);
border-radius: var(--vpl-c-border-radius);
border: 1px solid var(--vp-c-bg-soft);
padding: 24px 24px 20px 24px;
display: flex;
flex-direction: column;
flex-grow: 1;
h2 {
color: var(--vp-c-brand-1);
margin-top: -18px;
line-height: 24px;
font-size: 20px;
font-weight: 700;
max-width: 80%;
}
.collection-icon {
font-weight: 500;
font-size: 10px;
position: relative;
top: -20px;
right: -16px;
color: var(--vp-c-text-3);
.icon {
width: 10px;
}
}
.summary {
padding-top: 1em;
padding-bottom: 2em;
line-height: 24px;
font-size: 14px;
font-weight: 500;
color: var(--vp-c-text-2);
flex-grow: 1;
}
.attribution {
display: flex;
justify-content: space-between;
margin-top: 10px;
font-size: 14px;
font-weight: 500;
color: var(--vp-c-text-3);
.authors {
display: flex;
.avatars {
display: flex;
justify-content: flex-end;
.VPTeamMembersItem.icon {
overflow: visible;
&:not(:first-child) {
margin-left: -14px;
}
}
}
}
.date {
color: var(--vp-c-text-3);
}
.read-more {
color: var(--vp-c-brand-3);
}
}
}
@media (max-width: 767px) {
.collection-article-card {
width: auto;
}
}
@media (max-width: 420px) {
.attribution .authors .names {
display: none;
}
}
</style>

View File

@@ -0,0 +1,66 @@
<template>
<div
v-if="tags.length > 0"
class="aside-tags-wrapper"
>
<span class="ad-header">Tags</span>
<div class="aside-tags">
<Link
v-for="tag in tags"
:key="tag.key"
:no-icon="true"
:href="tag.href"
target="_self"
>
<Tag :text="tag.name" />
</Link>
</div>
</div>
</template>
<script setup>
import {computed} from 'vue';
import {useData} from 'vitepress';
import encodeTag from '../utils/encode-tag.js';
import Link from './VPLLink.vue';
import Tag from './VPLCollectionTag.vue';
const {frontmatter, theme} = useData();
const ptags = frontmatter?.value?.tags ?? [];
const tagLinkPattern = theme?.value?.tagLink;
const tags = computed(() => ptags.map(tag => {
// get tag details
const details = theme?.value?.tags?.[tag];
// set the link data
const data = {key: tag, name: tag, href: details?.link};
// if href is unset and we have a tagLinkPattern then use that
if (!data.href && tagLinkPattern && tagLinkPattern.includes(':tag-id')) data.href = tagLinkPattern.replace(':tag-id', encodeTag(tag));
// if href is unset and we have a tagLinkPattern then use that
if (!data.href && tagLinkPattern && tagLinkPattern.includes(':tag')) data.href = tagLinkPattern.replace(':tag', tag);
// just use the tagLink pattern
if (!data.href && tagLinkPattern) data.href = tagLinkPattern;
// return
return data;
}).filter(tag => tag.href !== undefined));
</script>
<style scoped>
.aside-tags-wrapper .ad-header {
margin: 0;
}
.aside-tags {
display: flex;
flex-direction: row;
flex-wrap: wrap;
padding: 12px 0;
gap: 5px;
.tag {
font-size: 10px;
}
}
</style>

View File

@@ -0,0 +1,137 @@
<template>
<div>
<div
:key="key"
class="collection-articles"
>
<div
v-for="(page, index) in pagination()"
:key="page.key"
:class="{'collection-article': true, grower: getGrower(index)}"
>
<Article
:page="page"
:size="props.size"
:more="props.more"
class="collection-page-article"
/>
</div>
</div>
<VPButton
v-if="pages.length > amount"
size="medium"
text="load more"
theme="alt"
class="load-more-button"
@click="adder"
>
load more
</VPButton>
</div>
</template>
<script setup>
import {defineAsyncComponent, onMounted, ref, watch} from 'vue';
import {VPButton} from 'vitepress/theme';
import Item from './VPLCollectionItem.vue';
const Article = defineAsyncComponent({
loader: async () => Item,
});
const props = defineProps({
items: {
type: Array,
required: true,
},
more: {
type: String,
default: 'readmore',
},
pager: {
type: Number,
default: 10,
},
size: {
type: String,
default: 'medium',
},
tags: {
type: Object,
default: () => ({}),
},
});
// Hardcoded pager value for now
const amount = ref(props.pager);
const key = ref(0);
// normalize data and sort
let pages = props.items
.map(item => Object.assign(item, {show: true, timestamp: item.date ? item.date : item.timestamp}))
.sort((a, b) => a.timestamp < b.timestamp ? 1 : -1);
const adder = () => amount.value += props.pager;
const getGrower = i => pagination()[i + 1] === undefined && (i + 1) % 2 !== 0;
const pagination = () => pages.slice(0, amount.value);
const filter = () => {
const tagList = Object.entries(props.tags).filter(pair => pair[1].selected === true).map(pair => pair[0]);
if (tagList.length === 0) return props.items;
return props.items.filter(item => Array.isArray(item.tags) && tagList.every(tag => item.tags.includes(tag)));
};
// recompute filter when tags change
watch(props.tags, () => {
pages = filter();
key.value++;
});
onMounted(() => {
pages = filter();
key.value++;
});
</script>
<style scoped>
.collection-articles {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
.collection-article {
max-width: 50%;
display: flex;
flex-grow: 1;
padding: 6px;
&.grower {
max-width: 100%;
}
}
}
.load-more-button {
margin: 24px 6px;
padding: 24px;
}
@media (max-width: 767px) {
.collection-articles {
.collection-article {
max-width: 100%;
}
}
}
@media (max-width: 420px) {
.collection-articles {
.collection-article {
padding: 6px 0;
}
}
}
</style>

View File

@@ -0,0 +1,72 @@
<template>
<div class="collection-page">
<slot />
</div>
</template>
<style scoped>
.collection-page {
padding: 0 16px 96px;
position: relative;
margin: 0 auto;
width: 100%;
}
@media (min-width: 768px) {
.collection-page {
padding-bottom: 128px;
}
}
:slotted(.collection-page-section + .collection-page-section),
:slotted(.collection-page-article + .collection-page-section) {
margin-top: 64px;
}
:slotted(.collection-page-article + .collection-page-article) {
margin-top: 24px;
}
@media (min-width: 768px) {
:slotted(.collection-page-title + .collection-page-section) {
margin-top: 16px;
}
:slotted(.collection-page-section + .collection-page-section),
:slotted(.collection-page-article + .collection-page-section) {
margin-top: 96px;
}
}
:slotted(.collection-page-article) {
padding: 0 24px;
}
@media (min-width: 768px) {
.collection-page {
padding: 0 32px 128px;
}
:slotted(.collection-page-article) {
padding: 0 48px;
}
}
@media (min-width: 960px) {
.collection-page {
padding: 0 64px 128px;
}
:slotted(.collection-page-article) {
padding: 0 64px;
}
}
@media (min-width: 1280px) {
.collection-page {
order: 1;
min-width: 640px;
max-width: 1200px;
margin: auto;
}
}
</style>

View File

@@ -0,0 +1,86 @@
<template>
<section class="collection-page-section">
<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.items"
class="items"
>
<slot name="items" />
</div>
</section>
</template>
<style scoped>
.collection-page-section {
padding: 0 32px;
}
@media (min-width: 768px) {
.collection-page-section {
padding: 0 48px;
}
}
@media (min-width: 960px) {
.collection-page-section {
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);
}
.items {
padding-top: 40px;
}
</style>

View File

@@ -0,0 +1,52 @@
<template>
<div class="collection-page-tags">
<Tag
v-for="(tag, name) in tags"
:key="name"
:type="tag.selected ? 'selected' : 'info'"
:text="name"
v-bind="tag"
@click="toggle(name)"
/>
</div>
</template>
<script setup>
import {onMounted} from 'vue';
import {useRoute} from 'vitepress';
import encodeTag from '../utils/encode-tag.js';
import Tag from './VPLCollectionTag.vue';
const tags = defineModel();
const toggle = tag => {
tags.value[tag].selected = !tags.value[tag].selected;
};
onMounted(() => {
const route = useRoute();
const params = route.tags ?? [];
for (const [tag] of Object.entries(tags.value)) {
tags.value[tag].selected = params.includes(tag) || params.includes(encodeTag(tag));
}
});
</script>
<style scoped>
.collection-page-tags {
display: flex;
flex-wrap: wrap;
gap: 5px;
justify-content: center;
align-items: center;
padding: 12px 0;
}
@media (max-width: 960px) {
.collection-page-tags {
padding: 12px 24px;
}
}
</style>

View File

@@ -0,0 +1,69 @@
<template>
<div class="collection-page-title">
<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>
.collection-page-title {
padding: 48px 32px;
text-align: center;
}
@media (min-width: 768px) {
.collection-page-title {
padding: 64px 48px 48px;
}
}
@media (min-width: 960px) {
.collection-page-title {
padding: 80px 64px 48px;
}
}
.title {
letter-spacing: 0;
line-height: 44px;
font-size: 36px;
font-weight: 500;
}
@media (min-width: 768px) {
.title {
letter-spacing: -0.5px;
line-height: 56px;
font-size: 48px;
}
}
.lead {
margin: 0 auto;
max-width: 512px;
padding-top: 12px;
line-height: 24px;
font-size: 16px;
font-weight: 500;
color: var(--vp-c-text-2);
}
@media (min-width: 768px) {
.lead {
max-width: 592px;
letter-spacing: 0.15px;
line-height: 28px;
font-size: 20px;
}
}
</style>

View File

@@ -0,0 +1,78 @@
<template>
<Badge
:class="`tag ${props.tagClass}`"
:style="props.type !== 'selected' ? styles : {}"
:type="props.type"
>
<span
v-if="icon"
class="icon"
v-html="icon"
/>
{{ props.text }}
</Badge>
</template>
<script setup>
import {computed} from 'vue';
import {useData} from 'vitepress';
const {theme} = useData();
const {tags} = theme.value;
const props = defineProps({
color: {
type: String,
default: 'none',
},
icon: {
type: String,
default: undefined,
},
styles: {
type: Object,
default: () => ({}),
},
tagClass: {
type: String,
default: '',
},
text: {
type: String,
required: true,
},
type: {
type: String,
default: 'info',
},
});
const details = Object.assign({color: props.color, styles: props.styles}, tags[props.text] ?? {});
const styles = computed(() => Object.assign({
'background-color': details.color,
'border-color': details.color,
}, details.styles));
const icon = computed(() => props.icon ?? details.icon ?? false);
</script>
<style scoped>
.tag {
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: space-between;
gap: 4px;
font-size: 14px;
.icon {
width: 16px;
}
}
.VPBadge.selected {
border-color: var(--vpl-badge-selected-border);
color: var(--vpl-badge-selected-text);
background-color: var(--vpl-badge-selected-bg);
}
</style>

View File

@@ -0,0 +1,290 @@
<template>
<footer
v-if="showFooter"
class="VPDocFooter"
>
<slot name="doc-footer-before" />
<div class="footer-box">
<div
v-if="hasBackLink"
class="back-link"
>
<Link :href="backLink.link">
{{ backLink.text ?? '<- Back' }}
</Link>
</div>
<div
v-if="hasContributors"
class="contributors"
>
<div class="contributors-flex">
<Contributor
v-for="contributor in contributors"
:key="contributor.key"
size="icon"
:member="contributor"
/>
</div>
</div>
<div class="empty" />
<div
v-if="hasEditLink || hasLastUpdated"
class="edit-info"
>
<div
v-if="hasEditLink"
class="edit-link"
>
<Link
class="edit-link-button"
:href="editLink?.url ?? editLink"
:no-icon="true"
>
<VPIconEdit
class="edit-link-icon"
aria-label="edit icon"
/>
{{ editLink?.text ?? theme?.value?.editLink?.text ?? 'Edit this page' }}
</Link>
</div>
<div
v-if="hasLastUpdated"
class="last-updated"
>
<DocFooterLastUpdated />
</div>
</div>
</div>
<nav
v-if="control.prev?.link || control.next?.link"
class="prev-next"
>
<div class="pager">
<Link
v-if="control.prev?.link"
class="pager-link prev"
:href="control.prev.link"
>
<span
class="desc"
v-html="theme.docFooter?.prev || 'Previous page'"
/>
<span
class="title"
v-html="control.prev.text"
/>
</Link>
</div>
<div class="pager">
<Link
v-if="control.next?.link"
class="pager-link next"
:href="control.next.link"
>
<span
class="desc"
v-html="theme.docFooter?.next || 'Next page'"
/>
<span
class="title"
v-html="control.next.text"
/>
</Link>
</div>
</nav>
</footer>
</template>
<script setup>
import {computed} from 'vue';
import {useData} from 'vitepress';
import {useEditLink} from 'vitepress/dist/client/theme-default/composables/edit-link';
import {usePrevNext} from 'vitepress/dist/client/theme-default/composables/prev-next';
import useCollection from '../client/use-collection.js';
import VPIconEdit from 'vitepress/dist/client/theme-default/components/icons/VPIconEdit.vue';
import Contributor from './VPLTeamMembersItem.vue';
import DocFooterLastUpdated from './VPLDocFooterLastUpdated.vue';
import Link from './VPLLink.vue';
const useBackLink = () => {
// if its a string then assume its the link
if (typeof frontmatter.value?.backLink === 'string') {
return computed(() => ({link: frontmatter.value.backLink}));
}
return computed(() => frontmatter.value.backLink);
};
const {theme, page, frontmatter} = useData();
const collection = computed(() => frontmatter.value?.collection ?? false);
const {prevnext} = useCollection(collection.value);
const oprevnext = usePrevNext();
const cprevnext = computed(() => {
const links = frontmatter.value?.collection ? prevnext : oprevnext;
return links.value;
});
const control = computed(() => ({
prev: frontmatter.value?.prev ? oprevnext.value.prev : cprevnext.value.prev,
next: frontmatter.value?.next ? oprevnext.value.next : cprevnext.value.next,
}));
const backLink = useBackLink();
const contributors = computed(() => frontmatter.value.contributors ?? page.value.contributors);
const editLink = frontmatter.value?.editLink ? computed(() => frontmatter.value?.editLink) : useEditLink();
const hasBackLink = computed(() => {
return frontmatter.value?.backLink?.link;
});
const hasContributors = computed(() => {
return contributors.value && contributors.value.length > 0;
});
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>
<style scoped>
.VPDocFooter {
margin-top: 64px;
}
.back-link {
display: flex;
align-items: flex-end;
border: 0;
line-height: 32px;
font-size: 14px;
font-weight: 500;
color: var(--vp-c-brand-1);
transition: color 0.25s;
}
.contributors {
max-width: 420px;
overflow: hidden;
max-height: 70px;
}
.contributors-flex {
height: 65px;
display: flex;
flex-direction: column;
flex-wrap: wrap;
justify-content: flex-end;
}
.desc {
display: block;
line-height: 20px;
font-size: 12px;
font-weight: 500;
color: var(--vp-c-text-2);
}
.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;
}
.footer-box {
display: flex;
justify-content: space-between;
align-items: stretch;
}
.prev-next {
border-top: 1px solid var(--vp-c-divider);
padding-top: 24px;
display: grid;
grid-row-gap: 8px;
}
.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;
}
.title {
display: block;
line-height: 20px;
font-size: 14px;
font-weight: 500;
color: var(--vp-c-brand-1);
transition: color 0.25s;
}
@media (max-width: 767px) {
.contributors {
max-width: 300px;
}
}
@media (max-width: 640px) {
.contributors {
display: none;
}
}
@media (min-width: 640px) {
.edit-info {
display: flex;
justify-content: space-between;
align-items: center;
}
}
@media (min-width: 640px) {
.prev-next {
grid-template-columns: repeat(2, 1fr);
grid-column-gap: 16px;
}
}
</style>

View File

@@ -0,0 +1,58 @@
<template>
<ClientOnly>
<p class="VPLastUpdated">
{{ theme.lastUpdated?.text || theme.lastUpdatedText || 'Last updated' }}
<time :datetime="isoDatetime">{{ datetime }}</time>
</p>
</ClientOnly>
</template>
<script setup>
import {ref, computed, watchEffect, onMounted} from 'vue';
import {useData} from 'vitepress';
import {format as timeago} from 'timeago.js';
const {theme, page, frontmatter, lang} = useData();
// handle time
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(() => {
// allow for timeago style
// @TODO: timeago has localization support but only en_US and zh_CN by default if we add support for this we need
// to do this in the component so the localization is relative to the users browser and not the build server
if (theme.value.lastUpdated?.formatOptions?.dateStyle === 'timeago') {
datetime.value = timeago(date.value.toLocaleDateString());
// otherwis the usual
} else {
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>
<style scoped>
.VPLastUpdated {
line-height: 24px;
font-size: 14px;
font-weight: 500;
color: var(--vp-c-text-2);
}
@media (min-width: 640px) {
.VPLastUpdated {
line-height: 32px;
font-size: 14px;
font-weight: 500;
}
}
</style>

View File

@@ -0,0 +1,60 @@
<template>
<Page>
<template #doc-top>
<img src="https://i.pinimg.com/originals/4c/d9/ce/4cd9ce636c6d5f23688f0fda99cd81cf.gif">
</template>
<template #doc-before>
<img src="https://i.pinimg.com/originals/4c/d9/ce/4cd9ce636c6d5f23688f0fda99cd81cf.gif">
</template>
<template #doc-after>
<img src="https://i.pinimg.com/originals/4c/d9/ce/4cd9ce636c6d5f23688f0fda99cd81cf.gif">
</template>
<template #doc-bottom>
<img src="https://i.pinimg.com/originals/4c/d9/ce/4cd9ce636c6d5f23688f0fda99cd81cf.gif">
</template>
<template #aside-top>
aside-top
</template>
<template #aside-outline-before>
aside-outline-before
</template>
<template #aside-outline-after>
aside-outline-after
</template>
<template #aside-ads-before>
aside-ads-before
</template>
<template #aside-ads-after>
aside-ads-after
</template>
<template #aside-bottom>
aside-bottom
</template>
<div class="cat-container">
CATVIBES
<Content />
</div>
</Page>
</template>
<script setup>
import Page from 'vitepress/dist/client/theme-default/components/VPDoc.vue';
</script>
<style lang="scss" scoped>
.contributors {
float: left;
max-width: 70%;
overflow: hidden;
max-height: 70px;
}
.contributors-flex {
height: 65px;
display: flex;
flex-direction: column;
flex-wrap: wrap;
justify-content: flex-end;
}
</style>

View File

@@ -0,0 +1,108 @@
<template>
<div
v-if="hasJobs"
class="jobs"
>
<span
v-if="props.title"
class="ad-header"
>
{{ props.title }}
</span>
<div
v-for="(job, index) in jobs"
:key="index"
class="job"
>
<a
:href="job.link"
target="_blank"
rel="noopener noreferrer"
>
<div class="job-image">
<img
:src="job.logo"
:alt="job.company"
>
</div>
<div class="job-info">
<div class="job-title">{{ job.title }}</div>
<div class="job-aux">
{{ job.company }} - {{ job.aux }}
</div>
</div>
</a>
</div>
</div>
</template>
<script setup>
import {computed} from 'vue';
import {useData} from 'vitepress';
const props = defineProps({
title: {
type: [String, Boolean],
default: 'Jobs',
},
});
const {theme, frontmatter} = useData();
const jobs = frontmatter.value.jobs ?? theme.value.jobs ?? [];
// Compute whether we end up with any jobs or not
const hasJobs = computed(() => jobs !== false && jobs.length > 0);
</script>
<style lang="scss" scoped>
.job {
background-color: var(--vp-carbon-ads-bg-color);
padding: 10px;
border-radius: var(--vpl-c-border-radius);
font-weight: 400;
margin-bottom: 10px;
a {
text-decoration: none;
display: flex;
align-items: center;
color: var(--vp-c-text-1);
font-weight: 700;
}
.job-image {
width: 34px;
margin-right: 5px;
text-decoration: none;
img {
max-height: 24px;
max-width: 24px;
}
}
.job-info {
width: 100%;
.job-title {
font-size: 14px;
}
.job-aux {
font-size: 10px;
letter-spacing: .3px;
color: var(--vp-c-brand-1);
font-weight: 400;
}
}
}
@media (max-width: 1500px) {
.rightbar {
.jobs {
display: none;
}
}
}
.read-mode {
.jobs {
display: none;
}
}
</style>

View File

@@ -0,0 +1,97 @@
<template>
<Layout :class="headerClass">
<template #layout-top>
<Alert
v-if="alert"
:key="alertKey"
:content="alert.content"
:closeable="alert.closeable"
:type="alert.type"
/>
</template>
<template #sidebar-nav-after>
<div
v-if="sidebarEnder !== false"
class="sidebar-end"
>
<VPSideBarItem
:depth="0"
:item="sidebarEnder"
/>
</div>
</template>
<template #doc-before>
<div
v-if="header !== ''"
class="collection-header"
>
<PostHeader v-if="header === 'post'" />
<CollectionHeader v-else />
</div>
</template>
<template #aside-ads-before>
<Tags :key="tagsKey" />
<Jobs :key="jobsKey" />
<Sponsors :key="sponsorsKey" />
</template>
<template #doc-footer-before>
<Tags
v-if="header === 'post'"
:key="tagsKey"
/>
<div
v-if="mailchimp"
class="newsletter-wrapper"
>
<MailChimp v-bind="mailchimp" />
</div>
</template>
</Layout>
</template>
<script setup>
import {useData} from 'vitepress';
import {computed, ref, watch} from 'vue';
import DefaultTheme from 'vitepress/theme';
import VPSideBarItem from 'vitepress/dist/client/theme-default/components/VPSidebarItem.vue';
import Alert from './VPLAlert.vue';
import CollectionHeader from './VPLCollectionHeader.vue';
import MailChimp from './VPLMailChimp.vue';
import PostHeader from './VPLPostHeader.vue';
import Tags from './VPLCollectionItemTags.vue';
const {Layout} = DefaultTheme;
let alertKey = ref(0);
let jobsKey = ref(0);
let sponsorsKey = ref(0);
let tagsKey = ref(0);
const {frontmatter, page, theme} = useData();
const alert = computed(() => frontmatter.value.alert ?? theme.value.alert ?? false);
const header = computed(() => frontmatter.value.collection || '');
const headerClass = computed(() => frontmatter.value.collection ? `collection-${frontmatter.value.collection}` : '');
const mailchimp = computed(() => frontmatter.value?.mailchimp?.action ? frontmatter.value.mailchimp : false);
const sidebarEnder = computed(() => theme.value.sidebarEnder ?? false);
watch(() => page.value.relativePath, () => {
alertKey = page.value.relativePath;
jobsKey = page.value.relativePath;
sponsorsKey = page.value.relativePath;
tagsKey = page.value.relativePath;
});
</script>
<style lang="scss" scoped>
.newsletter-wrapper {
border-top: 1px solid var(--vp-c-divider);
padding: 16px 0 ;
}
</style>

View File

@@ -0,0 +1,73 @@
<template>
<component
:is="tag"
class="VPLink"
:class="{
link: props.href,
'lando': true,
'vp-external-link-icon': isExternal,
'no-icon': noIcon
}"
:href="getLink(props.href)"
:target="target ?? (isExternal ? '_blank' : isFauxInternal ? '_self' : undefined)"
:rel="relation ?? (isExternal ? 'noreferrer' : undefined)"
>
<slot />
</component>
</template>
<script setup>
import {useData} from 'vitepress';
import {computed} from 'vue';
import {normalizeLink} from 'vitepress/dist/client/theme-default/support/utils.js';
import {default as checkIsFauxInternal} from '../utils/is-faux-internal';
import {default as normalizeMvb} from '../utils/normalize-mvblink';
import {default as normalizeRoot} from '../utils/normalize-rootlink';
const EXTERNAL_URL_RE = /^(?:[a-z]+:|\/\/)/i;
const {theme, site} = useData();
const {internalDomains} = theme.value;
const props = defineProps({
tag: {
type: String,
default: undefined,
},
href: {
type: String,
default: undefined,
},
noIcon: {
type: Boolean,
default: false,
},
target: {
type: String,
default: undefined,
},
rel: {
type: String,
default: undefined,
},
});
const relation = computed(() => {
if (props.rel === 'mvb') return 'alternate';
else if (props.rel === 'root' || props.rel === 'none') return undefined;
return props.rel;
});
const tag = computed(() => props.tag ?? (props.href ? 'a' : 'span'));
const target = computed(() => props.target ?? (props.rel === 'mvb' || props.rel === 'root' ? '_self' : undefined));
const isFauxInternal = computed(() => props.href && checkIsFauxInternal(props.href, internalDomains));
const isExternal = computed(() => !isFauxInternal.value && props.href && EXTERNAL_URL_RE.test(props.href));
const getLink = href => {
if (props.rel === 'mvb' && href) return normalizeMvb(href, site.value);
else if (props.rel === 'root' && href) return normalizeRoot(href, site.value);
return href ? normalizeLink(href) : undefined;
};
</script>

View File

@@ -0,0 +1,175 @@
<template>
<div class="newsletter post-subscribe">
<div class="newsletter__wrap">
<div class="newsletter__title">
<h3>{{ props.title }}</h3>
</div>
<div class="newsletter__content">
{{ props.byline }}
</div>
<div id="mc_embed_signup">
<form
id="mc-embedded-subscribe-form"
:action="props.action"
method="post"
name="mc-embedded-subscribe-form"
class="validate subscribe-form"
target="_blank"
novalidate
>
<input
id="mce-EMAIL"
v-model="email"
type="email"
placeholder="Email address"
name="EMAIL"
class="subscribe-input"
>
<div
id="mce-responses"
class="clear"
>
<div
id="mce-error-response"
class="response"
style="display:none"
/>
<div
id="mce-success-response"
class="response"
style="display:none"
/>
</div>
<div
style="position: absolute; left: -5000px;"
aria-hidden="true"
>
<input
type="text"
name="b_59874b4d6910fa65e724a4648_613837077f"
tabindex="-1"
value=""
>
</div>
<VPButton
size="big"
:text="props.button"
>
<input
id="mc-embedded-subscribe"
:class="{ disabled: !email }"
:disabled="!email"
type="submit"
name="subscribe"
value=""
>
</VPButton>
</form>
</div>
</div>
</div>
</template>
<script setup>
import {ref} from 'vue';
import {VPButton} from 'vitepress/theme';
const props = defineProps({
action: {
required: true,
type: String,
},
title: {
type: String,
default: 'Subscribe to the newsletter!',
},
byline: {
type: String,
default: null,
},
button: {
type: String,
default: 'Subscribe',
},
});
const email = ref(null);
</script>
<style lang="scss" scoped>
.newsletter {
text-align: center;
color: var(--vp-c-text-1);
margin: 16px 0;
}
.newsletter__wrap {
padding: 32px 32px;
border-radius: var(--vpl-c-border-radius);
box-sizing: border-box;
width: auto;
font-size: 14px;
input[type=email], input[type=text], textarea {
border: 0 solid #f8f8f8;
box-sizing: border-box;
height: 50px;
width: 100%;
padding: 1em;
&:focus {
outline: 1px solid var(--vp-c-brand-soft);
outline-offset: 2px;
}
}
}
.newsletter__title {
h3 {
margin: 0;
color: var(--vp-custom-block-brand-title);
font-weight: 600;
font-size: 18px;
text-transform: uppercase;
}
}
.newsletter__content {
margin: 16px 0;
line-height: 28px;
}
.post-subscribe {
.subscribe {
width: 100%;
padding: 0;
}
.subscribe-input {
background-color: var(--vp-c-bg);
font-size: inherit;
border: 1px solid var(--vp-c-bg-alt);
width: 100%;
padding: 0.6rem 1.2rem;
box-sizing: border-box;
border-radius: var(--vpl-c-border-radius);
outline: none;
height: auto;
margin: 1em 0;
}
.newsletter__wrap {
background-color: var(--vp-c-brand-soft);
}
@media (max-width: 767px) {
.post-subscribe .subscribe-form {
display: block;
}
}
}
@media (max-width: 420px) {
.newsletter {
padding-left: 0;
padding-right: 0;
border-radius: 0;
width: 100%;
}
.newsletter__wrap {
margin: 0.85rem -1.5rem;
border-radius: 0;
}
}
</style>

View File

@@ -0,0 +1,116 @@
<template>
<div :class="`VPMenuGroup ${getItemColumnsClass(props.columns)}`">
<p
v-if="props.text"
class="title"
>
{{ props.text }}
</p>
<div :class="{'VPMenuGroup-flex-wrapper': props.columns > 1}">
<template v-for="item in props.items">
<MenuLink
v-if="'link' in item"
:key="item.href"
:item="item"
/>
</template>
</div>
</div>
</template>
<script setup>
import MenuLink from './VPLMenuLink.vue';
const props = defineProps({
columns: {
type: Number,
default: 1,
},
items: {
type: Array,
default: () => ([]),
},
text: {
type: String,
default: undefined,
},
});
const getItemColumnsClass = columns => {
switch (columns) {
case 1:
return 'VPMenuGroup-columns-full';
case 2:
return 'VPMenuGroup-columns-half';
case 3:
return 'VPMenuGroup-columns-third';
case 4:
return 'VPMenuGroup-columns-quarter';
default:
return 'VPMenuGroup-colums-third';
};
};
</script>
<style scoped>
.VPMenuGroup {
margin: 12px -12px 0;
border-top: 1px solid var(--vp-c-divider);
padding: 12px 12px 0;
}
.VPMenuGroup .title {
color: var(--vp-c-text-3);
width: 100%;
}
.VPMenuGroup .VPMenuGroup-flex-wrapper {
width: 500px;
display: flex;
flex-wrap: wrap;
}
.VPMenuGroup-columns-full .VPMenuLink {
width: unset;
min-width: 100%;
}
.VPMenuGroup-columns-half .VPMenuLink {
min-width: 49%;
max-width: 50%;
}
.VPMenuGroup-columns-third .VPMenuLink {
min-width: 32%;
max-width: 33%;
}
.VPMenuGroup-columns-quarter .VPMenuLink {
min-width: 24%;
max-width: 25%;
}
.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 {
display: block;
padding: 0 12px;
line-height: 32px;
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-2);
white-space: nowrap;
transition: color 0.25s;
}
</style>

View File

@@ -0,0 +1,87 @@
<template>
<div class="VPMenuLink">
<Link
:class="{ active: isActive(page.relativePath, item.activeMatch || item.link, !!item.activeMatch) }"
:href="item.link"
:target="item.target"
:rel="item.rel"
>
{{ item.text }}
<Badge
v-if="hasAlert(item.alert) && isActiveAlert(item.alert)"
v-bind="getAlert(item.alert)"
/>
</Link>
</div>
</template>
<script setup>
import {toRefs} from 'vue';
import {useData} from 'vitepress';
import isActive from '../utils/is-active.js';
import Link from './VPLLink.vue';
const props = defineProps({
item: {
type: [Array, Object],
default: () => ([]),
},
});
const {item} = toRefs(props);
const {page} = useData();
const getAlert = alert => {
if (typeof alert === 'string') alert = {text: alert};
return {type: 'info', expires: 2000000000000, ...alert};
};
const hasAlert = alert => {
return (
alert
&& alert !== null
&& alert !== undefined
&& (typeof alert === 'string' || typeof alert == 'object')
);
};
const isActiveAlert = alert => {
const {expires} = getAlert(alert);
return new Date().getTime() < expires;
};
</script>
<style scoped>
.VPMenuGroup + .VPMenuLink {
margin: 12px -12px 0;
border-top: 1px solid var(--vp-c-divider);
padding: 12px 12px 0;
}
.VPMenuLink .VPBadge {
margin-top: 8px;
}
.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

@@ -0,0 +1,123 @@
<template>
<div class="maybe">
<span
v-if="treeHasNewAlerts"
class="alert-circle"
/>
<VPFlyout
:class="styles"
:button="item.text"
:items="item.items"
/>
</div>
</template>
<script setup>
import {computed, toRefs} from 'vue';
import {useData} from 'vitepress';
import isActive from '../utils/is-active.js';
import VPFlyout from 'vitepress/dist/client/theme-default/components/VPFlyout.vue';
const {page} = useData();
const props = defineProps({
item: {
type: [Array, Object],
default: () => ([]),
},
});
const {item} = toRefs(props);
const getAlert = alert => {
if (typeof alert === 'string') alert = {text: alert};
return {type: 'success', expires: 2000000000000, ...alert};
};
const isChildActive = navItem => {
if ('link' in navItem) {
return isActive(
page.value.relativePath,
props.item.activeMatch,
!!props.item.activeMatch,
);
} else {
return navItem.items.some(isChildActive);
}
};
const flattenTree = (data, collect = []) => {
// break up children and items
const {items, ...item} = data;
// collec the item
collect.push(item);
// if we have children we need to recurse and add
if (items && items.length > 0) {
items.map(child => {
collect.push(flattenTree(child));
});
};
// faltten and return
return collect.flat(Infinity);
};
const getClasses = item => {
const list = item.value.classes ?? item.value.class;
// if list is nully then
if (!list || list === null) return [];
// return
return Array.isArray(list) ? list : [list];
};
const hasAlert = item => {
const {alert} = item;
return (
alert
&& alert !== null
&& alert !== undefined
&& (typeof alert === 'string' || typeof alert == 'object')
);
};
const treeHasNewAlerts = computed(() => {
const items = flattenTree(item.value);
const activeAlerts = items
.filter(item => hasAlert(item))
.map(item => getAlert(item.alert))
.filter(alert => alert.type === 'new')
.filter(alert => alert && alert.expires > new Date().getTime());
return activeAlerts.length > 0;
});
const styles = computed(() => {
// get active status
const active = isActive(page.relativePath, item.activeMatch, !!item.activeMatch) || isChildActive(props.item);
// build class list
const classes = {active, VPNavBarMenuGroup: true, test: treeHasNewAlerts};
// handle custom classes
for (const style of getClasses(item)) classes[style] = true;
return classes;
});
</script>
<style scoped>
.maybe {
display: flex;
align-items: center;
}
.alert-circle {
height: 8px;
width: 8px;
background-color: var(--vp-c-brand-1);
border-radius: 50%;
margin-right: -7px;
margin-left: 8px;
}
</style>

View File

@@ -0,0 +1,119 @@
<template>
<div class="post-header">
<Icon
v-if="collection !== false"
:icon="icon"
:link="iconLink"
:title="collection"
/>
<span v-if="authors.length > 0">by</span>
<div
v-if="authors.length > 0"
class="post-avatars"
>
<Author
v-for="author in authors"
:key="author.name"
size="icon"
:member="author"
/>
</div>
<Link
v-for="(author, index) in authors"
:key="author.name"
:href="author.link"
no-icon
>
<span class="underline">{{ author.name }}</span><span class="separator">{{ getSeparator(index, authors.length) }}</span>
</Link>
<span v-if="hlocation">from</span>
<span
v-if="hlocation"
class="location"
>
{{ hlocation }}
</span>
on
<time
class="date"
:datetime="datetime"
>
{{ hdate }}
</time>
</div>
</template>
<script setup>
import {computed} from 'vue';
import {useData} from 'vitepress';
import Author from './VPLTeamMembersItem.vue';
import Icon from './VPLCollectionIcon.vue';
import Link from './VPLLink.vue';
const {frontmatter, page} = useData();
const authors = computed(() => frontmatter.value?.authors ?? false);
const collection = computed(() => frontmatter.value?.collection ?? false);
const datetime = computed(() => page.value?.datetime ?? false);
const icon = computed(() => page.value?.collection?.icon ?? false);
const iconLink = computed(() => page.value?.collection?.iconLink ?? false);
const getSeparator = (index, end = 0) => {
return index + 1 === end ? '' : ', ';
};
const hdate = computed(() => {
return new Date(frontmatter.value?.date ?? page.value?.lastUpdated ?? page.value?.timestamp).toLocaleDateString(undefined, {
year: 'numeric',
month: 'long',
day: 'numeric',
});
});
const hlocation = computed(() => {
return frontmatter.value?.location ?? authors?.[0]?.location ?? false;
});
</script>
<style lang="scss" scoped>
.post-header {
align-items: flex-start;
z-index: 1;
display: flex;
gap: 4px;
font-size: .75em;
margin-bottom: 24px;
a {
font-weight: 500;
color: color-mix(in srgb, var(--vp-c-brand-1) 90%, white);
.underline {
text-underline-offset: 2px;
text-decoration: underline;
}
.separator {
color: var(--vp-c-text-3);
text-decoration: none;
}
}
.location, .date {
font-weight: 700;
color: var(--vp-c-text-2);
}
.post-avatars {
display: flex;
justify-content: flex-end;
.VPTeamMembersItem.icon {
overflow: visible;
&:not(:first-child) {
margin-left: -14px;
}
}
}
}
</style>

View File

@@ -0,0 +1,213 @@
<template>
<div
v-if="hasSponsors"
class="sponsors"
>
<span
v-if="props.title"
class="ad-header"
>
{{ props.title }}
</span>
<div class="sponsors-wrapper">
<div
v-for="(sponsor, index) in sponsorList"
:key="index"
:class="sponsor.classes"
>
<div class="sponsor-inner">
<a
:href="sponsor.url"
target="_blank"
>
<div class="sponsor-image"><img
:src="sponsor.logo"
:alt="sponsor.name"
></div>
</a>
</div>
</div>
</div>
<div
v-if="props.text || props.link"
class="sponsor-footer"
>
<a
:href="props.link"
target="_blank"
>
<div class="sponsor sponsor-full">
<span class="sponsor-link">
{{ props.text }}
</span>
</div>
</a>
</div>
</div>
</template>
<script setup>
import yaml from 'js-yaml';
import {computed, onMounted, ref} from 'vue';
import {useData} from 'vitepress';
const extname = path => {
const file = path.split('/')[path.split('/').length - 1];
const fileparts = file.split('.');
return fileparts.length > 1 ? `.${fileparts[fileparts.length - 1]}` : undefined;
};
const {theme, frontmatter} = useData();
const sponsors = frontmatter.value.sponsors ?? theme.value.sponsors ?? [];
const props = defineProps({
text: {
type: [String, Boolean],
default: () => {
const {theme, frontmatter} = useData();
const sponsors = frontmatter.value.sponsors ?? theme.value.sponsors ?? [];
return sponsors.text ?? 'your logo?';
},
},
link: {
type: [String, Boolean],
default: () => {
const {theme, frontmatter} = useData();
const sponsors = frontmatter.value.sponsors ?? theme.value.sponsors ?? [];
return sponsors.link;
},
},
title: {
type: [String, Boolean],
default: 'SPONSORS',
},
});
// Set sponsor data to some reactive thing
const data = ref(sponsors.data ?? []);
// if data is a string/needs to be fetched then do that here
onMounted(async () => {
// if data is already an array then we good
if (Array.isArray(data.value)) return;
// otherwise it SHOULD be a url string
try {
const url = new URL(data.value);
const response = await fetch(url.href);
// allow special file extension handling
switch (extname(url.pathname)) {
case '.yaml':
case '.yml':
data.value = yaml.load(await response.text());
break;
default:
data.value = await response.json();
break;
};
} catch (error) {
console.error(`could not fetch and parse data from ${data.value}`);
console.error(error);
}
});
// Compute sponsor list
const sponsorList = computed(() => {
if (Array.isArray(data.value)) {
return data.value.map(sponsor => ({...sponsor, classes: `sponsor sponsor-${sponsor.type}`}));
} else {
return [];
}
});
// Compute whether we end up with any sponsors or not
const hasSponsors = computed(() => sponsors !== false && sponsors && sponsors.data && sponsors.data.length > 0);
</script>
<style lang="scss" scoped>
.sponsors-wrapper {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
}
.sponsor {
height: 50px;
width: 33%;
display: flex;
align-items: center;
justify-content: space-around;
margin-top: 2px;
cursor: pointer;
border-radius: var(--vpl-c-border-radius);
.sponsor-inner {
background-color: var(--vp-carbon-ads-bg-color);
width: 100%;
height: 100%;
margin-left: 1px;
margin-right: 1px;
display: flex;
align-items: center;
justify-content: space-around;
border-radius: var(--vpl-c-border-radius);
}
&.sponsor-half {
width: 50%;
}
&.sponsor-full {
width: 100%;
margin-bottom: 10px;
}
.sponsor-image {
display: flex;
justify-content: space-around;
align-items: center;
padding: 5px;
img {
max-height: 40px;
max-width: 80%;
}
}
}
.sponsor-footer {
margin-top: 10px;
.sponsor {
display: flex;
justify-content: space-around;
align-items: center;
width: auto;
background-color: var(--vp-carbon-ads-bg-color);
width: 100%;
height: 50px;
margin-left: 1px;
margin-right: 1px;
display: flex;
align-items: center;
justify-content: space-around;
.sponsor-link {
color: var(--vp-c-text-3);
display: block;
font-weight: 700;
font-size: 11px;
text-transform: uppercase;
letter-spacing: .4px;
}
}
}
@media (max-width: 1500px) {
.rightbar {
.sponsors {
display: none;
}
}
}
.read-mode {
.sponsors {
display: none;
}
}
</style>

View File

@@ -0,0 +1,225 @@
<template>
<VPTeamMembers
:size="size"
:members="members"
/>
</template>
<script setup>
import useTeam from '../client/use-team.js';
import {VPTeamMembers} from 'vitepress/theme';
const {members, size} = defineProps({
size: {
type: String,
default: 'medium',
},
members: {
type: Array,
default: () => {
return useTeam() ?? [];
},
},
});
</script>
<style scoped>
.VPTeamMembersItem {
display: flex;
flex-direction: column;
gap: 2px;
border-radius: 12px;
width: 100%;
height: 100%;
overflow: hidden;
}
.VPTeamMembersItem.icon {
display: flex;
flex-direction: column;
gap: 2px;
border-radius: 12px;
width: 30px;
height: 30px;
overflow: hidden;
}
.VPTeamMembersItem.icon .profile {
padding: 0px;
background-color: transparent;
}
.VPTeamMembersItem.icon .data {
display: none;
}
.VPTeamMembersItem.icon .avatar {
width: 24px;
height: 24px;
box-shadow: none;
}
.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

@@ -0,0 +1,392 @@
<template>
<article
class="VPTeamMembersItem"
:class="[size, maintainerClass]"
>
<div class="profile">
<div
v-if="(member.commits || member.maintainer) && size !== 'icon'"
class="top-hat"
>
<div class="maintainer-role">
{{ member.maintainer ? 'Maintainer' : '' }}
</div>
<div class="commits">
{{ member.commits ? member.commits : '' }}
</div>
</div>
<figure class="avatar">
<Link
:href="getLink(member)"
no-icon
>
<img
class="avatar-img"
:src="avatar"
:alt="`Picture of ${member.name}`"
:title="getAvatarTitle(member)"
>
</Link>
</figure>
<div class="data">
<div class="name">
{{ member.name }}
</div>
<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>
<Link
v-if="member.org"
class="org"
:class="{ link: member.orgLink }"
:href="member.orgLink"
no-icon
>
{{ member.org }}
</Link>
</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"
>
<Link
class="sp-link"
:href="member.sponsor"
no-icon
>
<VPIconHeart class="sp-icon" /> Sponsor
</Link>
</div>
</article>
</template>
<script setup>
import {computed} from 'vue';
import VPIconHeart from 'vitepress/dist/client/theme-default/components/icons/VPIconHeart.vue';
import VPSocialLinks from 'vitepress/dist/client/theme-default/components/VPSocialLinks.vue';
import Link from './VPLLink.vue';
const {member, size} = defineProps({
size: {
type: String,
default: 'medium',
},
member: {
type: Object,
default: () => ({}),
},
});
// compute avatar url with correct size
const avatar = computed(() => {
const src = member.avatar ?? member.pic;
switch (size) {
case 'icon':
return `${src}?size=24`;
case 'small':
return `${src}?size=64`;
case 'medium':
return `${src}?size=120`;
case 'large':
return `${src}?size=256`;
default:
return src;
};
});
const maintainerClass = computed(() => member.maintainer ? 'maintainer' : '');
const getLink = member => {
if (member.link) return member.link;
else if (Array.isArray(member?.links) && member.links[0]) return member.links[0].link;
else if (member.email) return `mailto:${member.email}`;
};
const getAvatarTitle = member => {
let avatarTitle = `${member.name}`;
if (member.email) avatarTitle += ` <${member.email}>`;
if (member.commits) avatarTitle += ` - ${Number.parseInt(member.commits, 10)} commits`;
return avatarTitle;
};
</script>
<style scoped>
.VPTeamMembersItem {
display: flex;
flex-direction: column;
gap: 2px;
border-radius: 12px;
width: 100%;
height: 100%;
overflow: hidden;
}
.VPTeamMembersItem.icon {
display: flex;
flex-direction: column;
gap: 2px;
border-radius: 12px;
width: 30px;
height: 30px;
overflow: hidden;
}
.VPTeamMembersItem.icon .profile {
padding: 0px;
background-color: transparent;
}
.VPTeamMembersItem.icon .top-hat {
display: none;
}
.VPTeamMembersItem.icon .maintainer-role {
display: none;
}
.VPTeamMembersItem.icon .commits {
display: none;
}
.VPTeamMembersItem.icon .data {
display: none;
}
.VPTeamMembersItem.icon .avatar {
width: 24px;
height: 24px;
box-shadow: none;
}
.VPTeamMembersItem.icon .sp {
display: none;
}
.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: 12px;
}
.VPTeamMembersItem.small .desc {
padding-top: 12px;
line-height: 20px;
font-size: 14px;
display: none;
}
.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: 14px;
}
.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(--vpl-c-bg-contributor);
}
.maintainer .profile {
background-color: var(--vpl-c-bg-maintainer);
}
.maintainer .sp-link {
background-color: var(--vpl-c-bg-maintainer);
}
.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;
text-transform: uppercase;
font-weight: 700;
color: var(--vp-c-text-3);
}
.at {
color: var(--vp-c-text-2);
}
.top-hat {
position: relative;
top: -30px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.commits {
color: var(--vp-c-text-3);
position: relative;
right: -25px;
font-size: 10px
}
.maintainer-role {
color: var(--vp-c-text-3);
position: relative;
font-size: 10px;
left: -25px;
text-transform: uppercase;
font-weight: 700;
}
.org.link {
color: var(--vp-c-text-3);
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

@@ -0,0 +1,100 @@
<template>
<a
class="VPLink"
:class="{
'prerelease': props.prerelease,
link: props.version ?? props.text,
'vp-external-link-icon': props.target === '_blank',
'no-icon': props.noIcon
}"
:href="getLink(props.version ?? props.text)"
:target="props.target"
>
{{ props.version ?? props.text }}
<Badge
v-if="props.stable"
type="success"
text="STABLE"
vertical="middle"
/>
<Badge
v-if="props.edge"
type="warning"
text="EDGE"
vertical="middle"
/>
<Badge
v-if="props.dev"
type="tip"
text="DEV"
vertical="middle"
/>
</a>
</template>
<script setup>
import {default as normalizeMvb} from '../utils/normalize-mvblink';
import {useData} from 'vitepress';
const {site} = useData();
const props = defineProps({
dev: {
type: Boolean,
default: false,
},
edge: {
type: Boolean,
default: false,
},
noIcon: {
type: Boolean,
default: false,
},
prerelease: {
type: Boolean,
default: false,
},
stable: {
type: Boolean,
default: false,
},
target: {
type: String,
default: '_blank',
},
version: {
type: String,
default: undefined,
},
// DEPRECATED but kept for backwards compat
text: {
type: String,
default: undefined,
},
});
const getLink = version => {
if (props.dev === true) return normalizeMvb('/dev/', site.value);
return normalizeMvb(`/${version}/`, site.value);
};
</script>
<style lang="scss">
.version-link {
a {
color: var(--vp-c-green-3);
&:hover {
color: var(--vp-c-green-3);
}
}
a.prerelease {
color: var(--vp-c-yellow-3);
&:hover {
color: var(--vp-c-yellow-3);
}
}
}
</style>

View File

@@ -0,0 +1,57 @@
<template>
<div
v-if="url"
class="video-responsive"
>
<iframe
width="100%"
height="400"
:src="url"
frameborder="0"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
/>
</div>
</template>
<script setup>
import {computed} from 'vue';
const props = defineProps({
id: {
type: String,
required: true,
},
});
const url = computed(() => `https://www.youtube.com/embed/${props.id}`);
</script>
<style lang="scss">
.video-responsive {
margin-top: 1em;
overflow: hidden;
padding-bottom: 56.25%;
position: relative;
height: 0;
iframe {
left: 0;
top: 0;
height: 100%;
width: 100%;
position: absolute;
}
}
@media (max-width: 767px) {
.video-responsive {
padding-left: 0;
padding-right: 0;
border-radius: 0;
width: auto;
margin: 0.85rem -1.5rem;
border-radius: 0;
}
}
</style>