feat(sponsor): add sponsors component

pull/639/head
Kia King Ishii 3 years ago
parent 905b4a0142
commit 6f037e2688

@ -6,19 +6,11 @@ export default defineConfig({
description: 'Vite & Vue powered static site generator.',
themeConfig: {
nav: [
{ text: 'Guide', link: '/guide/what-is-vitepress' },
{ text: 'Configs', link: '/config/app-configs' },
{
text: 'Release Notes',
link: 'https://github.com/vuejs/vitepress/releases'
}
],
nav: nav(),
sidebar: {
'/guide/': getGuideSidebar(),
'/config/': getConfigSidebar(),
// '/': getGuideSidebar()
'/guide/': sidebarGuide(),
'/config/': sidebarConfig()
},
editLink: {
@ -32,6 +24,11 @@ export default defineConfig({
{ icon: 'github', link: 'https://github.com/vuejs/vitepress' }
],
footer: {
message: 'Released under the MIT License.',
copyright: 'Copyright © 2019-present Evan You'
},
algolia: {
appId: '8J64VVRP8K',
apiKey: 'a18e2f4cc5665f6602c5631fd868adfd',
@ -40,16 +37,40 @@ export default defineConfig({
}
})
function getGuideSidebar() {
function nav() {
return [
{ text: 'Guide', link: '/guide/what-is-vitepress' },
{ text: 'Configs', link: '/config/introduction' },
{
text: 'Release Notes',
link: 'https://github.com/vuejs/vitepress/releases'
}
]
}
function sidebarGuide() {
return [
{
text: 'Introduction',
items: [{ text: 'What is VitePress?', link: '/guide/what-is-vitepress' }]
},
{
text: 'Migrations',
items: [
{
text: 'Migration from VuePress',
link: '/guide/migration-from-vuepress'
},
{
text: 'Migration from VitePress 0.x',
link: '/guide/migration-from-vitepress-0'
}
]
}
]
}
function getConfigSidebar() {
function sidebarConfig() {
return [
{
text: 'Config',

@ -71,7 +71,7 @@ export default {
- Type: `boolean`
- Default: `true`
Whether to enable "Dark Mode" or not. If the option is set to `true`, it adds `.dark` class to the `<html>` tag.
Whether to enable "Dark Mode" or not. If the option is set to `true`, it adds `.dark` class to the `<html>` tag depending on the users preference.
It also injects inline script that tries to read users settings from local storage by `vitepress-theme-appearance` key and restores users preferred color mode.

@ -72,18 +72,80 @@ hero:
name: VuePress
text: Vite & Vue powered static site generator.
tagline: Lorem ipsum...
actions:
- theme: brand
text: Get Started
link: /guide/what-is-vitepress
- theme: alt
text: View on GitHub
link: https://github.com/vuejs/vitepress
---
```
```ts
interface Hero {
// The string shown top of `text`. Best used for product name.
name: string
// The string shown top of `text`. Comes with brand color
// and expected to be short, such as product name.
name?: string
// The main text for the hero section. This will be defined as `h1`.
// The main text for the hero section. This will be defined
// as `h1` tag.
text: string
// Tagline displayed below `text`.
tagline: string
tagline?: string
// Action buttons to display in home hero section.
actions?: HeroAction[]
}
interface HeroAction {
// Color theme of the button. Defaults to `brand`.
theme?: 'brand' | 'alt'
// Label of the button.
text: string
// Destination link of the button.
link: string
}
```
## features
- Type: `Feature[]`
This option only take effect when `layout` is set to `home`.
It defines items to display in features section.
```yaml
---
layout: home
features:
- icon: ⚡️
title: Vite, The DX that can't be beat
details: Lorem ipsum...
- icon: 🖖
title: Power of Vue meets Markdown
details: Lorem ipsum...
- icon: 🛠️
title: Simple and minimal, always
details: Lorem ipsum...
---
```
```ts
interface Feature {
// Show icon on each feature box. Currently, only emojis
// are supported.
icon?: string
// Title of the feature.
title: string
// Details of the feature.
details: string
}
```

@ -1,3 +1,58 @@
# Theme Configs
Coming soon...
Theme configs let you customize your theme. You can define theme configs by adding `themeConfig` key to the config file.
```ts
export default {
lang: 'en-US',
title: 'VitePress',
description: 'Vite & Vue powered static site generator.',
// Theme related configurations.
themeConfig: {
logo: '/logo.svg',
nav: [...],
sidebar: { ... }
}
}
```
Here it describes the settings for the VitePress default theme. If you're using a custom theme created by others, these settings may not have any effect, or might behave differently.
## logo
- Type: `string`
Logo file to display in nav bar, right before the site title.
```ts
export default {
themeConfig: {
logo: '/logo.svg'
}
}
```
## footer
- Type: `Footer`
Footer configuration. You can add a message and copyright. The footer will displayed only when the page doesn't contain sidebar due to design reason.
```ts
export default {
themeConfig: {
footer: {
message: 'Released under the MIT License.',
copyright: 'Copyright © 2019-present Evan You'
}
}
}
```
```ts
export interface Footer {
message?: string
copyright?: string
}
```

@ -0,0 +1,3 @@
# Migration from VitePress 0.x
Coming soon...

@ -0,0 +1,3 @@
# Migration from VuePress
Coming soon...

@ -2,7 +2,24 @@
layout: home
hero:
name: VuePress
name: VitePress
text: Vite & Vue powered static site generator.
tagline: Simple, minimal, yet powerful as lightning. Meet the modern SSG framework you've always wanted.
tagline: Simple, minimal, yet strikingly powerful. Meet the modern SSG framework you've always wanted.
actions:
- theme: brand
text: Get Started
link: /guide/what-is-vitepress
- theme: alt
text: View on GitHub
link: https://github.com/vuejs/vitepress
features:
- title: Vite, The DX that can't be beat
details: Feel the speed of Vite. Instant server start and lightning fast HMR that stays fast regardless of the app size.
- title: Power of Vue meets Markdown
details: Enjoy the outstanding feature set of Vue Component in markdown, and develop custom themes with Vue.
- title: Simple and minimal, always
details: The project structure that helps you focus on writing, yet fully customizable for any website development.
- title: Fully static, but dynammic
details: Go wild with the true SSG + SPA architecture. Static on the page load, engage users with 100% interactive from there.
---

@ -7,6 +7,7 @@ import VPNav from './components/VPNav.vue'
import VPLocalNav from './components/VPLocalNav.vue'
import VPSidebar from './components/VPSidebar.vue'
import VPContent from './components/VPContent.vue'
import VPFooter from './components/VPFooter.vue'
const {
isOpen: isSidebarOpen,
@ -26,6 +27,22 @@ provide('close-sidebar', closeSidebar)
<VPNav />
<VPLocalNav :open="isSidebarOpen" @open-menu="openSidebar" />
<VPSidebar :open="isSidebarOpen" />
<VPContent />
<VPContent>
<template #home-hero-before><slot name="home-hero-before" /></template>
<template #home-hero-after><slot name="home-hero-after" /></template>
<template #home-features-before><slot name="home-features-before" /></template>
<template #home-features-after><slot name="home-features-after" /></template>
</VPContent>
<VPFooter />
</div>
</template>
<style scoped>
.Layout {
display: flex;
flex-direction: column;
min-height: 100vh;
}
</style>

@ -17,7 +17,7 @@ defineProps<{
right: 0;
bottom: 0;
left: 0;
z-index: var(--vp-z-backdrop);
z-index: var(--vp-z-index-backdrop);
background: rgba(0, 0, 0, .6);
transition: opacity 0.5s;
}

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

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

@ -15,21 +15,38 @@ const { hasSidebar } = useSidebar()
<div
class="VPContent"
id="VPContent"
:class="{ 'has-sidebar': hasSidebar }"
:class="{
'has-sidebar': hasSidebar,
'is-home': frontmatter.layout === 'home'
}"
>
<NotFound v-if="route.component === NotFound" />
<VPPage v-else-if="frontmatter.layout === 'page'" />
<VPHome v-else-if="frontmatter.layout === 'home'" />
<VPHome v-else-if="frontmatter.layout === 'home'">
<template #home-hero-before><slot name="home-hero-before" /></template>
<template #home-hero-after><slot name="home-hero-after" /></template>
<template #home-features-before><slot name="home-features-before" /></template>
<template #home-features-after><slot name="home-features-after" /></template>
</VPHome>
<VPDoc v-else :class="{ 'has-sidebar': hasSidebar }" />
</div>
</template>
<style scoped>
.VPContent {
flex-grow: 1;
margin: 0 auto;
max-width: var(--vp-layout-max-width);
}
.VPContent.is-home {
width: 100%;
max-width: 100%;
}
@media (max-width: 768px) {
.VPContent {
overflow-x: hidden;

@ -0,0 +1,53 @@
<script setup lang="ts">
import { useData } from 'vitepress'
import { useSidebar } from '../composables/sidebar'
const { theme } = useData()
const { hasSidebar } = useSidebar()
</script>
<template>
<footer v-if="theme.footer" class="VPFooter" :class="{ 'has-sidebar': hasSidebar }">
<div class="container">
<p class="message">{{ theme.footer.message }}</p>
<p class="copyright">{{ theme.footer.copyright }}</p>
</div>
</footer>
</template>
<style scoped>
.VPFooter {
position: relative;
z-index: var(--vp-z-index-footer);
border-top: 1px solid var(--vp-c-divider-light);
padding: 32px 24px;
background-color: var(--vp-c-bg-content);
}
.VPFooter.has-sidebar {
display: none;
}
@media (min-width: 768px) {
.VPFooter {
padding: 32px;
}
}
.container {
margin: 0 auto;
max-width: var(--vp-layout-max-width);
text-align: center;
}
.message,
.copyright {
line-height: 24px;
font-size: 14px;
font-weight: 500;
color: var(--vp-c-text-2);
}
.message { order: 2; }
.copyright { order: 1; }
</style>

@ -0,0 +1,144 @@
<script setup lang="ts">
import VPButton from './VPButton.vue'
interface HeroAction {
theme?: 'brand' | 'alt'
text: string
link: string
}
defineProps<{
name?: string
text: string
tagline?: string
actions?: HeroAction[]
}>()
</script>
<template>
<div class="VPHero">
<div class="container">
<p v-if="name" class="name"><span class="clip">{{ name }}</span></p>
<h1 v-if="text" class="text">{{ text }}</h1>
<p v-if="tagline" class="tagline">{{ tagline }}</p>
<div v-if="actions" class="actions">
<div v-for="action in actions" :key="action.link" class="action">
<VPButton
tag="a"
size="big"
:theme="action.theme"
:text="action.text"
:href="action.link"
/>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.VPHero {
margin-top: calc(var(--vp-nav-height) * -1);
padding: calc(var(--vp-nav-height) + 48px) 24px 48px;
}
@media (min-width: 640px) {
.VPHero {
padding: calc(var(--vp-nav-height) + 80px) 48px 64px;
}
}
.container {
margin: 0 auto;
max-width: 960px;
}
.name,
.text {
line-height: 40px;
font-size: 32px;
font-weight: 700;
}
.name {
padding-bottom: 4px;
color: var(--vp-home-hero-name-color);
}
.clip {
background: var(--vp-home-hero-name-background);
-webkit-text-fill-color: var(--vp-home-hero-name-color);
-webkit-background-clip: text;
}
@media (min-width: 640px) {
.name,
.text {
max-width: 640px;
line-height: 56px;
font-size: 48px;
}
.name {
padding-bottom: 6px;
}
}
@media (min-width: 960px) {
.name,
.text {
max-width: 720px;
line-height: 64px;
font-size: 56px;
}
.name {
padding-bottom: 6px;
}
}
.tagline {
padding-top: 16px;
line-height: 28px;
font-size: 18px;
font-weight: 500;
color: var(--vp-c-text-2);
}
@media (min-width: 640px) {
.tagline {
padding-top: 24px;
max-width: 540px;
line-height: 32px;
font-size: 20px;
}
}
@media (min-width: 960px) {
.tagline {
padding-top: 24px;
max-width: 640px;
line-height: 36px;
font-size: 24px;
}
}
.actions {
display: flex;
flex-wrap: wrap;
margin: -8px;
padding-top: 24px;
}
@media (min-width: 640px) {
.actions {
padding-top: 32px;
}
}
.action {
flex-shrink: 0;
padding: 8px;
}
</style>

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

@ -0,0 +1,96 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useData } from 'vitepress'
import VPBox from './VPBox.vue'
const { frontmatter: fm } = useData()
const grid = computed(() => {
const length = fm.value.features?.length
if (!length) {
return
}
if (length === 2) {
return 'grid-2'
} else if (length === 3) {
return 'grid-3'
} else if (length % 3 === 0) {
return 'grid-6'
} else if (length % 2 === 0) {
return 'grid-4'
}
})
</script>
<template>
<div v-if="fm.features" class="VPHomeFeatures">
<div class="container">
<div class="items">
<div v-for="feature in fm.features" :key="feature.title" class="item" :class="[grid]">
<VPBox
:icon="feature.icon"
:title="feature.title"
:details="feature.details"
/>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.VPHomeFeatures {
padding: 0 24px;
}
@media (min-width: 640px) {
.VPHomeFeatures {
padding: 0 48px;
}
}
.container {
margin: 0 auto;
max-width: 960px;
}
.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>

@ -1,106 +1,17 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useData } from 'vitepress'
import VPHero from './VPHero.vue'
const { frontmatter: fm } = useData()
</script>
<template>
<div v-if="fm.hero" class="VPHomeHero">
<div class="container">
<p v-if="fm.hero.name" class="name">{{ fm.hero.name }}</p>
<h1 v-if="fm.hero.text" class="text">{{ fm.hero.text }}</h1>
<p v-if="fm.hero.tagline" class="tagline">{{ fm.hero.tagline }}</p>
</div>
<VPHero
:name="fm.hero.name"
:text="fm.hero.text"
:tagline="fm.hero.tagline"
:actions="fm.hero.actions"
/>
</div>
</template>
<style scoped>
.VPHomeHero {
margin-top: calc(var(--vp-nav-height) * -1);
padding: calc(var(--vp-nav-height) + 48px) 24px 56px;
}
@media (min-width: 640px) {
.VPHomeHero {
padding: calc(var(--vp-nav-height) + 80px) 48px 96px;
}
}
@media (min-width: 960px) {
.VPHomeHero {
padding: calc(var(--vp-nav-height) + 80px) 32px 96px;
}
}
.container {
margin: 0 auto;
max-width: 960px;
}
.name,
.text {
line-height: 44px;
font-size: 32px;
font-weight: 700;
}
.name {
padding-bottom: 4px;
color: var(--vp-c-brand);
}
@media (min-width: 640px) {
.name,
.text {
max-width: 920px;
line-height: 56px;
font-size: 48px;
font-weight: 600;
}
.name {
padding-bottom: 6px;
}
}
@media (min-width: 960px) {
.name,
.text {
max-width: 920px;
line-height: 72px;
font-size: 64px;
font-weight: 600;
}
.name {
padding-bottom: 6px;
}
}
.tagline {
padding-top: 16px;
line-height: 32px;
font-size: 18px;
font-weight: 500;
color: var(--vp-c-text-2);
}
@media (min-width: 640px) {
.tagline {
padding-top: 24px;
max-width: 540px;
line-height: 32px;
font-size: 20px;
}
}
@media (min-width: 960px) {
.tagline {
padding-top: 32px;
max-width: 720px;
line-height: 40px;
font-size: 24px;
}
}
</style>

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

@ -3,9 +3,9 @@ import '@docsearch/css'
import { useData } from 'vitepress'
import { defineAsyncComponent, ref, onMounted, onUnmounted } from 'vue'
const VPAlgoliaSearchBox = defineAsyncComponent(
() => import('./VPAlgoliaSearchBox.vue')
)
const VPAlgoliaSearchBox = defineAsyncComponent(() => {
return import('./VPAlgoliaSearchBox.vue')
})
const { theme } = useData()

@ -0,0 +1,77 @@
<script setup lang="ts">
import { computed } from 'vue'
import VPSponsorsGrid from './VPSponsorsGrid.vue'
interface Sponsors {
tier?: string
size?: 'small' | 'medium' | 'big'
items: Sponsor[]
}
interface Sponsor {
name: string
img: string
url: string
}
const props = defineProps<{
tier?: string
size?: 'small' | 'medium' | 'big'
data: Sposors[] | Sponsor[]
}>()
const sponsors = computed(() => {
const isSponsors = props.data.some((s: any) => s.items)
if (isSponsors) {
return props.data
}
return [{
tier: props.tier,
size: props.size,
items: props.data
}]
})
</script>
<template>
<div class="VPSponsors">
<section v-for="(sponsor, index) in sponsors" :key="index" class="section">
<h3 v-if="sponsor.tier" class="tier">{{ sponsor.tier }}</h3>
<VPSponsorsGrid :size="sponsor.size" :data="sponsor.items" />
</section>
</div>
</template>
<style scoped>
.VPSponsors {
border-radius: 16px;
overflow: hidden;
}
.section + .section {
margin-top: 4px;
}
.tier {
margin-bottom: 4px;
padding: 13px 0 11px;
text-align: center;
letter-spacing: 1px;
line-height: 24px;
width: 100%;
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-2);
background-color: var(--vp-c-white-soft);
}
.dark .tier {
background-color: var(--vp-c-black-mute);
}
.VPSponsorsGrid + .tier {
margin-top: 4px;
}
</style>

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

@ -0,0 +1,5 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M12,22.2c-0.3,0-0.5-0.1-0.7-0.3l-8.8-8.8c-2.5-2.5-2.5-6.7,0-9.2c2.5-2.5,6.7-2.5,9.2,0L12,4.3l0.4-0.4c0,0,0,0,0,0C13.6,2.7,15.2,2,16.9,2c0,0,0,0,0,0c1.7,0,3.4,0.7,4.6,1.9l0,0c1.2,1.2,1.9,2.9,1.9,4.6c0,1.7-0.7,3.4-1.9,4.6l-8.8,8.8C12.5,22.1,12.3,22.2,12,22.2zM7,4C5.9,4,4.7,4.4,3.9,5.3c-1.8,1.8-1.8,4.6,0,6.4l8.1,8.1l8.1-8.1c0.9-0.9,1.3-2,1.3-3.2c0-1.2-0.5-2.3-1.3-3.2l0,0C19.3,4.5,18.2,4,17,4c0,0,0,0,0,0c-1.2,0-2.3,0.5-3.2,1.3c0,0,0,0,0,0l-1.1,1.1c-0.4,0.4-1,0.4-1.4,0l-1.1-1.1C9.4,4.4,8.2,4,7,4z" />
</svg>
</template>

@ -1,6 +1,7 @@
import { Ref, onMounted, onUpdated, onUnmounted } from 'vue'
import { Header } from 'vitepress'
import { useMediaQuery } from '@vueuse/core'
import { throttleAndDebounce } from '../support/utils'
interface HeaderWithChildren extends Header {
children?: Header[]
@ -162,24 +163,3 @@ function isAnchorActive(
return [false, null]
}
function throttleAndDebounce(fn: () => void, delay: number): () => void {
let timeout: number
let called = false
return () => {
if (timeout) {
clearTimeout(timeout)
}
if (!called) {
fn()
called = true
setTimeout(() => {
called = false
}, delay)
} else {
timeout = setTimeout(fn, delay)
}
}
}

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

@ -3,6 +3,7 @@ import './styles/vars.css'
import './styles/base.css'
import './styles/utils.css'
import './styles/vp-doc.css'
import './styles/vp-sponsor.css'
import { Theme } from 'vitepress'
import Layout from './Layout.vue'
@ -10,6 +11,10 @@ import NotFound from './NotFound.vue'
export { DefaultTheme } from './config'
export { default as VPHomeHero } from './components/VPHomeHero.vue'
export { default as VPHomeFeatures } from './components/VPHomeFeatures.vue'
export { default as VPHomeSponsors } from './components/VPHomeSponsors.vue'
const theme: Theme = {
Layout,
NotFound

@ -52,6 +52,9 @@
--vp-c-indigo: #213547;
--vp-c-indigo-soft: #476582;
--vp-c-indigo-light: #aac8e4;
--vp-c-indigo-lighter: #c9def1;
--vp-c-indigo-dark: #1d2f3f;
--vp-c-indigo-darker: #14212e;
--vp-c-blue: #3b8eed;
--vp-c-blue-light: #549ced;
@ -109,7 +112,11 @@
--vp-c-brand: var(--vp-c-green);
--vp-c-brand-light: var(--vp-c-green-light);
--vp-c-brand-lighter: var(--vp-c-green-lighter);
--vp-c-brand-dark: var(--vp-c-green-dark);
--vp-c-brand-darker: var(--vp-c-green-darker);
--vp-c-sponsor: #fd1d7c;
}
.dark {
@ -168,8 +175,9 @@
:root {
--vp-z-index-local-nav: 10;
--vp-z-index-nav: 20;
--vp-z-backdrop: 30;
--vp-z-index-backdrop: 30;
--vp-z-index-sidebar: 40;
--vp-z-index-footer: 50;
}
/**
@ -221,13 +229,71 @@
}
/**
* Component: Home
* Component: Button
* -------------------------------------------------------------------------- */
:root {
--vp-button-brand-border: var(--vp-c-brand-light);
--vp-button-brand-text: var(--vp-c-text-dark-1);
--vp-button-brand-bg: var(--vp-c-brand);
--vp-button-brand-hover-border: var(--vp-c-brand-light);
--vp-button-brand-hover-text: var(--vp-c-text-dark-1);
--vp-button-brand-hover-bg: var(--vp-c-brand-light);
--vp-button-brand-active-border: var(--vp-c-brand-light);
--vp-button-brand-active-text: var(--vp-c-text-dark-1);
--vp-button-brand-active-bg: var(--vp-button-brand-bg);
--vp-button-alt-border: var(--vp-c-gray-light-3);
--vp-button-alt-text: var(--vp-c-text-light-1);
--vp-button-alt-bg: var(--vp-c-gray-light-5);
--vp-button-alt-hover-border: var(--vp-c-gray-light-3);
--vp-button-alt-hover-text: var(--vp-c-text-light-1);
--vp-button-alt-hover-bg: var(--vp-c-gray-light-4);
--vp-button-alt-active-border: var(--vp-c-gray-light-3);
--vp-button-alt-active-text: var(--vp-c-text-light-1);
--vp-button-alt-active-bg: var(--vp-c-gray-light-3);
--vp-button-sponsor-border: var(--vp-c-gray-light-3);
--vp-button-sponsor-text: var(--vp-c-text-light-2);
--vp-button-sponsor-bg: transparent;
--vp-button-sponsor-hover-border: var(--vp-c-sponsor);
--vp-button-sponsor-hover-text: var(--vp-c-sponsor);
--vp-button-sponsor-hover-bg: transparent;
--vp-button-sponsor-active-border: var(--vp-c-sponsor);
--vp-button-sponsor-active-text: var(--vp-c-sponsor);
--vp-button-sponsor-active-bg: transparent;
}
.dark {
--vp-button-brand-border: var(--vp-c-brand-lighter);
--vp-button-brand-text: var(--vp-c-text-light-1);
--vp-button-brand-bg: var(--vp-c-brand-light);
--vp-button-brand-hover-border: var(--vp-c-brand-lighter);
--vp-button-brand-hover-text: var(--vp-c-text-light-1);
--vp-button-brand-hover-bg: var(--vp-c-brand-lighter);
--vp-button-brand-active-border: var(--vp-c-brand-lighter);
--vp-button-brand-active-text: var(--vp-c-text-light-1);
--vp-button-brand-active-bg: var(--vp-button-brand-bg);
--vp-button-alt-border: var(--vp-c-gray-dark-2);
--vp-button-alt-text: var(--vp-c-text-dark-1);
--vp-button-alt-bg: var(--vp-c-bg-mute);
--vp-button-alt-hover-border: var(--vp-c-gray-dark-2);
--vp-button-alt-hover-text: var(--vp-c-text-dark-1);
--vp-button-alt-hover-bg: var(--vp-c-gray-dark-3);
--vp-button-alt-active-border: var(--vp-c-gray-dark-2);
--vp-button-alt-active-text: var(--vp-c-text-dark-1);
--vp-button-alt-active-bg: var(--vp-button-alt-bg);
--vp-button-sponsor-border: var(--vp-c-gray-dark-1);
--vp-button-sponsor-text: var(--vp-c-text-dark-2);
}
/**
* Component: Home
* -------------------------------------------------------------------------- */
:root {
--vp-home-hero-name-color: var(--vp-c-brand);
--vp-home-hero-name-background: transparent;
}

@ -221,7 +221,7 @@
@media (min-width: 640px) {
.vp-doc div[class*='language-'] {
border-radius: 8px;
margin: 20px 0;
margin: 24px 0;
}
}

@ -0,0 +1,86 @@
.vp-sponsor-grid {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.vp-sponsor-grid.small .vp-sponsor-grid-link { height: 96px; }
.vp-sponsor-grid.small .vp-sponsor-grid-image { max-width: 96px; max-height: 24px }
.vp-sponsor-grid.medium .vp-sponsor-grid-link { height: 112px; }
.vp-sponsor-grid.medium .vp-sponsor-grid-image { max-width: 120px; max-height: 36px }
.vp-sponsor-grid.big .vp-sponsor-grid-link { height: 184px; }
.vp-sponsor-grid.big .vp-sponsor-grid-image { max-width: 192px; max-height: 56px }
.vp-sponsor-grid[data-vp-grid="2"] .vp-sponsor-grid-item {
width: calc((100% - 4px) / 2);
}
.vp-sponsor-grid[data-vp-grid="3"] .vp-sponsor-grid-item {
width: calc((100% - 4px * 2) / 3);
}
.vp-sponsor-grid[data-vp-grid="4"] .vp-sponsor-grid-item {
width: calc((100% - 4px * 3) / 4);
}
.vp-sponsor-grid[data-vp-grid="5"] .vp-sponsor-grid-item {
width: calc((100% - 4px * 4) / 5);
}
.vp-sponsor-grid[data-vp-grid="6"] .vp-sponsor-grid-item {
width: calc((100% - 4px * 5) / 6);
}
.vp-sponsor-grid-item {
flex-shrink: 0;
width: 100%;
background-color: var(--vp-c-white-soft);
transition: background-color 0.25s;
}
.vp-sponsor-grid-item:hover {
background-color: var(--vp-c-white-mute);
}
.vp-sponsor-grid-item:hover .vp-sponsor-grid-image {
filter: grayscale(0) invert(0);
}
.vp-sponsor-grid-item.empty:hover {
background-color: var(--vp-c-white-soft);
}
.dark .vp-sponsor-grid-item {
background-color: var(--vp-c-black-mute);
}
.dark .vp-sponsor-grid-item:hover {
background-color: var(--vp-c-white-soft);
}
.dark .vp-sponsor-grid-item.empty:hover {
background-color: var(--vp-c-black-mute);
}
.vp-sponsor-grid-link {
display: flex;
}
.vp-sponsor-grid-box {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
}
.vp-sponsor-grid-image {
max-width: 100%;
filter: grayscale(1);
transition: filter 0.25s;
}
.dark .vp-sponsor-grid-image {
filter: grayscale(1) invert(1);
}

@ -12,6 +12,27 @@ export function isExternal(path: string): boolean {
return OUTBOUND_RE.test(path)
}
export function throttleAndDebounce(fn: () => void, delay: number): () => void {
let timeout: number
let called = false
return () => {
if (timeout) {
clearTimeout(timeout)
}
if (!called) {
fn()
called = true
setTimeout(() => {
called = false
}, delay)
} else {
timeout = setTimeout(fn, delay)
}
}
}
export function isActive(
currentPath: string,
matchPath?: string,

@ -29,6 +29,11 @@ export namespace DefaultTheme {
*/
socialLinks?: SocialLink[]
/**
* The footer configuration.
*/
footer?: Footer
/**
* Adds locale menu to the nav. This option should be used when you have
* your translated sites outside of the project.
@ -133,6 +138,13 @@ export namespace DefaultTheme {
| 'twitter'
| 'youtube'
// footer --------------------------------------------------------------------
export interface Footer {
message?: string
copyright?: string
}
// locales -------------------------------------------------------------------
export interface LocaleLinks {

Loading…
Cancel
Save