feat(theme): add navbar components

This commit is contained in:
pengzhanbo 2023-02-11 18:59:38 +08:00
parent 86f51ff271
commit 4b0078d010
33 changed files with 1383 additions and 119 deletions

View File

@ -19,31 +19,32 @@ export default defineUserConfig({
theme: themePlume({
logo: 'https://pengzhanbo.cn/g.gif',
hostname: 'https://pengzhanbo.cn',
appearance: true,
avatar: {
url: '/images/blogger.jpg',
name: 'Plume Theme',
description: 'The Theme for Vuepress 2.0',
},
social: {
email: 'volodymyr@foxmail.com',
github: 'pengzhanbo',
QQ: '942450674',
weiBo: 'https://weibo.com',
zhiHu: 'https://zhihu.com',
facebook: 'https://baidu.com',
twitter: 'https://baidu.com',
linkedin: 'https://baidu.com',
},
social: [{ icon: 'github', link: 'https://github.com/pengzhanbo' }],
// {
// email: 'volodymyr@foxmail.com',
// github: 'pengzhanbo',
// QQ: '942450674',
// weiBo: 'https://weibo.com',
// zhiHu: 'https://zhihu.com',
// facebook: 'https://baidu.com',
// twitter: 'https://baidu.com',
// linkedin: 'https://baidu.com',
// },
notes,
darkMode: true,
navbar: [
{
text: 'VuePress',
children: [
items: [
{ text: 'theme-plume', link: '/note/vuepress-theme-plume/' },
{
text: 'Plugin',
children: [
items: [
{ text: 'caniuse', link: '/note/vuepress-plugin/caniuse/' },
{
text: 'netlify-functions',

View File

@ -101,7 +101,7 @@ function initSidebar(note: NotesItem, pages: NotePage[]): NotesSidebarItem[] {
}
function initSidebarByConfig(
{ text, link, dir, sidebar }: NotesItem,
{ text, dir, sidebar }: NotesItem,
pages: NotePage[]
): NotesSidebarItem[] {
return (sidebar as NotesSidebar).map((item) => {
@ -114,11 +114,11 @@ function initSidebarByConfig(
items: [],
}
} else {
// link = path.join(link || '', item.link || '')
const current = findNotePage(item.link || '', dir, pages)
return {
text: item.text || item.dir || current?.title,
link: current?.link,
collapsed: item.collapsed,
items: initSidebarByConfig(
{
link: item.link || '',

View File

@ -19,6 +19,7 @@ export type NotesSidebarItem = {
text?: string
link?: string
dir?: string
collapsed?: boolean
items?: NotesSidebar
}

View File

@ -0,0 +1,42 @@
<script lang="ts" setup>
import { computed } from 'vue'
import { EXTERNAL_URL_RE, normalizeLink } from '../utils/index.js'
import IconExternalLink from './icons/IconExternalLink.vue'
const props = defineProps<{
tag?: string
href?: string
noIcon?: boolean
}>()
const tag = computed(() => (props.tag ?? props.href ? 'a' : 'span'))
const isExternal = computed(
() => props.href && EXTERNAL_URL_RE.test(props.href)
)
</script>
<template>
<Component
:is="tag"
class="auto-link"
:class="{ link: href }"
:href="href ? normalizeLink(href) : undefined"
:target="isExternal ? '_blank' : undefined"
:rel="isExternal ? 'noreferrer' : undefined"
>
<slot />
<IconExternalLink v-if="isExternal && !noIcon" class="icon" />
</Component>
</template>
<style scoped>
.icon {
display: inline-block;
margin-top: -1px;
margin-left: 4px;
width: 11px;
height: 11px;
fill: var(--vp-c-text-3);
transition: fill 0.25s;
}
</style>

View File

@ -0,0 +1,46 @@
<script lang="ts" setup>
import MenuLink from './MenuLink.vue'
defineProps<{
text?: string
items: any[]
}>()
</script>
<template>
<div class="menu-group">
<p v-if="text" class="title">{{ text }}</p>
<template v-for="item in items">
<MenuLink v-if="'link' in item" :key="item?.link" :item="item" />
</template>
</div>
</template>
<style scoped>
.menu-group {
margin: 12px -12px 0;
border-top: 1px solid var(--vp-c-divider);
padding: 12px 12px 0;
}
.menu-group:first-child {
margin-top: 0;
border-top: 0;
padding-top: 0;
}
.menu-group + .menu-group {
margin-top: 12px;
border-top: 1px solid var(--vp-c-divider);
}
.title {
padding: 0 12px;
line-height: 32px;
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-2);
transition: color 0.25s;
}
</style>

View File

@ -0,0 +1,57 @@
<script lang="ts" setup>
import { usePageData } from '@vuepress/client'
import { isActive } from '../../utils/index.js'
import AutoLink from '../AutoLink.vue'
defineProps<{
item: any
}>()
const page = usePageData()
</script>
<template>
<div class="menu-link">
<AutoLink
:class="{
active: isActive(
page.path,
item.activeMatch || item.link,
!!item.activeMatch
),
}"
:href="item.link"
>
{{ item.text }}
</AutoLink>
</div>
</template>
<style scoped>
.menu-group + .menu-link {
margin: 12px -12px 0;
border-top: 1px solid var(--vp-c-divider);
padding: 12px 12px 0;
}
.link {
display: block;
border-radius: 6px;
padding: 0 12px;
line-height: 32px;
font-size: 14px;
font-weight: 500;
color: var(--vp-c-text-1);
white-space: nowrap;
transition: background-color 0.25s, color 0.25s;
}
.link:hover {
color: var(--vp-c-brand);
background-color: var(--vp-c-bg-elv-mute);
}
.link.active {
color: var(--vp-c-brand);
}
</style>

View File

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

View File

@ -0,0 +1,138 @@
<script lang="ts" setup>
import { ref } from 'vue'
import { useFlyout } from '../../composables/flyout.js'
import IconChevronDown from '../icons/IconChevronDown.vue'
import IconMoreHorizontal from '../icons/IconMoreHorizontal.vue'
import VMenu from './VMenu.vue'
defineProps<{
icon?: any
button?: string
label?: string
items?: any[]
}>()
const open = ref(false)
const el = ref<HTMLElement>()
useFlyout({ el, onBlur })
function onBlur() {
open.value = false
}
</script>
<template>
<div
ref="el"
class="flyout-wrapper"
@mouseenter="open = true"
@mouseleave="open = false"
>
<button
type="button"
class="button"
aria-haspopup="true"
:aria-expanded="open"
:aria-label="label"
@click="open = !open"
>
<span v-if="button || icon" class="text">
<Component :is="icon" v-if="icon" class="option-icon" />
{{ button }}
<IconChevronDown class="text-icon" />
</span>
<IconMoreHorizontal v-else class="icon" />
</button>
<div class="menu">
<VMenu :items="items">
<slot />
</VMenu>
</div>
</div>
</template>
<style scoped>
.flyout-wrapper {
position: relative;
}
.flyout-wrapper:hover {
color: var(--vp-c-brand);
transition: color 0.25s;
}
.flyout-wrapper:hover .text {
color: var(--vp-c-text-2);
}
.flyout-wrapper:hover .icon {
fill: var(--vp-c-text-2);
}
.flyout-wrapper.active .text {
color: var(--vp-c-brand);
}
.flyout-wrapper.active:hover .text {
color: var(--vp-c-brand-dark);
}
.flyout-wrapper:hover .menu,
.button[aria-expanded='true'] + .menu {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.button {
display: flex;
align-items: center;
padding: 0 12px;
height: var(--vp-nav-height);
color: var(--vp-c-text-1);
transition: color 0.5s;
}
.text {
display: flex;
align-items: center;
line-height: var(--vp-nav-height);
font-size: 14px;
font-weight: 500;
color: var(--vp-c-text-1);
transition: color 0.25s;
}
.option-icon {
margin-right: 0px;
width: 16px;
height: 16px;
fill: currentColor;
}
.text-icon {
margin-left: 4px;
width: 14px;
height: 14px;
fill: currentColor;
}
.icon {
width: 20px;
height: 20px;
fill: currentColor;
transition: fill 0.25s;
}
.menu {
position: absolute;
top: calc(var(--vp-nav-height) / 2 + 20px);
right: 0;
opacity: 0;
visibility: hidden;
transition: opacity 0.25s, visibility 0.25s, transform 0.25s;
}
</style>

View File

@ -2,6 +2,11 @@
import { useWindowScroll } from '@vueuse/core'
import { computed } from 'vue'
import { useSidebar } from '../../composables/sidebar.js'
import NavBarAppearance from './NavBarAppearance.vue'
import NavBarExtra from './NavBarExtra.vue'
import NavBarHamburger from './NavBarHamburger.vue'
import NavBarMenu from './NavBarMenu.vue'
import NavBarSocialLinks from './NavBarSocialLinks.vue'
import NavBarTitle from './NavBarTitle.vue'
defineProps<{
@ -31,6 +36,15 @@ const classes = computed(() => ({
<div class="curtain"></div>
<div class="content-body">
<NavbarSearch class="search" />
<NavBarMenu class="menu" />
<NavBarAppearance class="appearance" />
<NavBarSocialLinks class="social-links" />
<NavBarExtra class="extra" />
<NavBarHamburger
class="hamburger"
:active="isScreenOpen"
@click="$emit('toggle-screen')"
/>
</div>
</div>
</div>

View File

@ -0,0 +1,25 @@
<script lang="ts" setup>
import { useThemeLocaleData } from '../../composables/index.js'
import SwitchAppearance from '../SwitchAppearance.vue'
const theme = useThemeLocaleData()
</script>
<template>
<div v-if="theme.appearance" class="navbar-appearance">
<SwitchAppearance />
</div>
</template>
<style scoped>
.navbar-appearance {
display: none;
}
@media (min-width: 1280px) {
.navbar-appearance {
display: flex;
align-items: center;
}
}
</style>

View File

@ -0,0 +1,78 @@
<script lang="ts" setup>
import { computed } from 'vue'
import { useThemeLocaleData } from '../../composables/index.js'
import Flyout from '../Flyout/index.vue'
import SocialLinks from '../SocialLinks.vue'
import SwitchAppearance from '../SwitchAppearance.vue'
const theme = useThemeLocaleData()
const hasExtraContent = computed(
() => theme.value.appearance || theme.value.social
)
</script>
<template>
<Flyout v-if="hasExtraContent" class="navbar-extra" label="extra navigation">
<div v-if="theme.appearance" class="group">
<div class="item appearance">
<p class="label">Appearance</p>
<div class="appearance-action">
<SwitchAppearance />
</div>
</div>
</div>
<div v-if="theme.social" class="group">
<div class="item social-links">
<SocialLinks class="social-links-list" :links="theme.social" />
</div>
</div>
</Flyout>
</template>
<style scoped>
.navbar-extra {
display: none;
margin-right: -12px;
}
@media (min-width: 768px) {
.navbar-extra {
display: block;
}
}
@media (min-width: 1280px) {
.navbar-extra {
display: none;
}
}
.trans-title {
padding: 0 24px 0 12px;
line-height: 32px;
font-size: 14px;
font-weight: 700;
color: var(--vp-c-text-1);
}
.item.appearance,
.item.social-links {
display: flex;
align-items: center;
padding: 0 12px;
}
.item.appearance {
min-width: 176px;
}
.appearance-action {
margin-right: -2px;
}
.social-links-list {
margin: -4px -8px;
}
</style>

View File

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

View File

@ -0,0 +1,35 @@
<script lang="ts" setup>
import { useThemeLocaleData } from '../../composables/themeData.js'
import NavBarMenuGroup from './NavBarMenuGroup.vue'
import NavBarMenuLink from './NavBarMenuLink.vue'
const theme = useThemeLocaleData()
</script>
<template>
<nav
v-if="theme.navbar"
aria-labelledby="main-nav-aria-label"
class="navbar-menu"
>
<span id="main-nav-aria-label" class="visually-hidden"
>Main Navigation</span
>
<template v-for="item in theme.navbar" :key="item.text">
<NavBarMenuLink v-if="'link' in item" :item="item" />
<NavBarMenuGroup v-else :item="item" />
</template>
</nav>
</template>
<style scoped>
.navbar-menu {
display: none;
}
@media (min-width: 768px) {
.navbar-menu {
display: flex;
}
}
</style>

View File

@ -0,0 +1,23 @@
<script lang="ts" setup>
import { usePageData } from '@vuepress/client'
import type { NavItemWithChildren } from '../../../shared/index.js'
import { isActive } from '../../utils/index.js'
import Flyout from '../Flyout/index.vue'
defineProps<{
item: NavItemWithChildren
}>()
const page = usePageData()
</script>
<template>
<Flyout
:class="{
'navbar-menu-group': true,
'active': isActive(page.path, item.activeMatch, !!item.activeMatch),
}"
:button="item.text"
:items="item.items"
/>
</template>

View File

@ -0,0 +1,50 @@
<script lang="ts" setup>
import { usePageData } from '@vuepress/client'
import type { NavItemWithLink } from '../../../shared/index.js'
import { isActive } from '../../utils/index.js'
import AutoLink from '../AutoLink.vue'
defineProps<{
item: NavItemWithLink
}>()
const page = usePageData()
</script>
<template>
<AutoLink
:class="{
'navbar-menu-link': true,
'active': isActive(
page.path,
item.activeMatch || item.link,
!!item.activeMatch
),
}"
:href="item.link"
:no-icon="true"
>
{{ item.text }}
</AutoLink>
</template>
<style scoped>
.navbar-menu-link {
display: flex;
align-items: center;
padding: 0 12px;
line-height: var(--vp-nav-height);
font-size: 14px;
font-weight: 500;
color: var(--vp-c-text-1);
transition: color 0.25s;
}
.navbar-menu-link.active {
color: var(--vp-c-brand);
}
.navbar-menu-link:hover {
color: var(--vp-c-brand);
}
</style>

View File

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

View File

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

View File

@ -0,0 +1,27 @@
<script lang="ts" setup>
import type { SocialLink as SocialLinkType } from '../../shared/index.js'
import SocialLink from './SocialLink.vue'
defineProps<{
links: SocialLinkType[]
}>()
</script>
<template>
<div class="social-links">
<SocialLink
v-for="{ link, icon } in links"
:key="link"
:icon="icon"
:link="link"
/>
</div>
</template>
<style scoped>
.social-links {
display: flex;
flex-wrap: wrap;
justify-content: center;
}
</style>

View File

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

View File

@ -0,0 +1,118 @@
<script lang="ts" setup>
import { onMounted, ref, watch } from 'vue'
import { useDarkMode } from '../composables/index.js'
import { useThemeLocaleData } from '../composables/themeData.js'
import { APPEARANCE_KEY } from '../utils/index.js'
import IconMoon from './icons/IconMoon.vue'
import IconSun from './icons/IconSun.vue'
import Switch from './Switch.vue'
const theme = useThemeLocaleData()
const checked = ref(false)
const isDark = useDarkMode()
// eslint-disable-next-line @typescript-eslint/no-empty-function
const toggle = typeof localStorage !== 'undefined' ? useAppearance() : () => {}
onMounted(() => {
checked.value = document.documentElement.classList.contains('dark')
})
function useAppearance() {
const query = window.matchMedia('(prefers-color-scheme: dark)')
const classList = document.documentElement.classList
let userPreference = localStorage.getItem(APPEARANCE_KEY)
let isDark =
(theme.value.appearance === 'dark' && userPreference == null) ||
(userPreference === 'auto' || userPreference == null
? query.matches
: userPreference === 'dark')
query.onchange = (e) => {
if (userPreference === 'auto') {
setClass((isDark = e.matches))
}
}
function toggle() {
setClass((isDark = !isDark))
userPreference = isDark
? query.matches
? 'auto'
: 'dark'
: query.matches
? 'light'
: 'auto'
localStorage.setItem(APPEARANCE_KEY, userPreference)
}
function setClass(dark: boolean): void {
const css = document.createElement('style')
css.type = 'text/css'
css.appendChild(
document.createTextNode(
`:not(.VPSwitchAppearance):not(.VPSwitchAppearance *) {
-webkit-transition: none !important;
-moz-transition: none !important;
-o-transition: none !important;
-ms-transition: none !important;
transition: none !important;
}`
)
)
document.head.appendChild(css)
checked.value = dark
classList[dark ? 'add' : 'remove']('dark')
// eslint-disable-next-line @typescript-eslint/naming-convention
const _ = window.getComputedStyle(css).opacity
document.head.removeChild(css)
}
return toggle
}
watch(checked, (newIsDark) => {
isDark.value = newIsDark
})
</script>
<template>
<Switch
class="switch-appearance"
aria-label="toggle dark mode"
:aria-checked="checked"
@click="toggle"
>
<IconSun class="sun" />
<IconMoon class="moon" />
</Switch>
</template>
<style scoped>
.sun {
opacity: 1;
}
.moon {
opacity: 0;
}
.dark .sun {
opacity: 0;
}
.dark .moon {
opacity: 1;
}
.dark .switch-appearance :deep(.check) {
/*rtl:ignore*/
transform: translateX(18px);
}
</style>

View File

@ -1,7 +1,5 @@
import { usePreferredDark, useStorage } from '@vueuse/core'
import { computed, inject, onMounted, onUnmounted, provide, watch } from 'vue'
import { inject, provide, ref } from 'vue'
import type { InjectionKey, WritableComputedRef } from 'vue'
import { useThemeLocaleData } from './themeData.js'
export type DarkModeRef = WritableComputedRef<boolean>
@ -13,60 +11,17 @@ export const darkModeSymbol: InjectionKey<DarkModeRef> = Symbol(
* Inject dark mode global computed
*/
export const useDarkMode = (): DarkModeRef => {
const isDarkMode = inject(darkModeSymbol)
if (!isDarkMode) {
const isDark = inject(darkModeSymbol)
if (isDark === undefined) {
throw new Error('useDarkMode() is called without provider.')
}
return isDarkMode
return isDark
}
/**
* Create dark mode ref and provide as global computed in setup
*/
export const setupDarkMode = (): void => {
const themeLocale = useThemeLocaleData()
const isDarkPreferred = usePreferredDark()
const darkStorage = useStorage(
'vuepress-color-scheme',
themeLocale.value.colorMode
)
const isDarkMode = computed<boolean>({
get() {
// disable color mode switching
if (!themeLocale.value.colorModeSwitch) {
return themeLocale.value.colorMode === 'dark'
}
// auto detected from prefers-color-scheme
if (darkStorage.value === 'auto') {
return isDarkPreferred.value
}
// storage value
return darkStorage.value === 'dark'
},
set(val) {
if (val === isDarkPreferred.value) {
darkStorage.value = 'auto'
} else {
darkStorage.value = val ? 'dark' : 'light'
}
},
})
provide(darkModeSymbol, isDarkMode)
updateHtmlDarkClass(isDarkMode)
}
export const updateHtmlDarkClass = (isDarkMode: DarkModeRef): void => {
const update = (value = isDarkMode.value): void => {
// set `class="dark"` on `<html>` element
const htmlEl = window?.document.querySelector('html')
htmlEl?.classList.toggle('dark', value)
}
onMounted(() => {
watch(isDarkMode, update, { immediate: true })
})
onUnmounted(() => update())
const isDark = ref<boolean>(false)
provide(darkModeSymbol, isDark)
}

View File

@ -0,0 +1,59 @@
import { onUnmounted, readonly, ref, type Ref, watch } from 'vue'
import { inBrowser } from '../utils/index.js'
interface UseFlyoutOptions {
el: Ref<HTMLElement | undefined>
onFocus?(): void
onBlur?(): void
}
export const focusedElement = ref<HTMLElement>()
let active = false
let listeners = 0
export function useFlyout(options: UseFlyoutOptions) {
const focus = ref(false)
if (inBrowser) {
!active && activateFocusTracking()
listeners++
const unwatch = watch(focusedElement, (el) => {
if (el === options.el.value || options.el.value?.contains(el!)) {
focus.value = true
options.onFocus?.()
} else {
focus.value = false
options.onBlur?.()
}
})
onUnmounted(() => {
unwatch()
listeners--
if (!listeners) {
deactivateFocusTracking()
}
})
}
return readonly(focus)
}
function activateFocusTracking() {
document.addEventListener('focusin', handleFocusIn)
active = true
focusedElement.value = document.activeElement as HTMLElement
}
function deactivateFocusTracking() {
document.removeEventListener('focusin', handleFocusIn)
}
function handleFocusIn() {
focusedElement.value = document.activeElement as HTMLElement
}

View File

@ -1,11 +1,169 @@
import { computed } from 'vue'
import type {
NotesData,
NotesSidebarItem,
} from '@vuepress-plume/vuepress-plugin-notes-data'
import { useNotesData } from '@vuepress-plume/vuepress-plugin-notes-data/client'
import type { PageData } from '@vuepress/client'
import { usePageData, usePageFrontmatter, withBase } from '@vuepress/client'
import { useMediaQuery } from '@vueuse/core'
import type { ComputedRef, Ref } from 'vue'
import { computed, onMounted, onUnmounted, ref, watchEffect } from 'vue'
import { useRoute } from 'vue-router'
import { isActive } from '../utils/index.js'
import { useThemeLocaleData } from './themeData.js'
export function getSidebarList(path: string, notesData: NotesData) {
const link = Object.keys(notesData).find((link) =>
path.startsWith(withBase(link))
)
return link ? notesData[link] : []
}
export function useSidebar() {
const route = useRoute()
const notesData = useNotesData()
const theme = useThemeLocaleData()
const frontmatter = usePageFrontmatter()
const is960 = useMediaQuery('(min-width: 960px)')
const isOpen = ref(false)
const sidebar = computed(() => {
return theme.value.notes ? getSidebarList(route.path, notesData.value) : []
})
const hasSidebar = computed(() => {
return false
return !frontmatter.value.home && sidebar.value.length > 0
})
const hasAside = computed(() => {
return !frontmatter.value.home && frontmatter.value.aside !== false
})
const isSidebarEnabled = computed(() => hasSidebar.value && is960.value)
function open() {
isOpen.value = true
}
function close() {
isOpen.value = false
}
function toggle() {
isOpen.value ? close() : open()
}
return {
isOpen,
sidebar,
hasSidebar,
hasAside,
isSidebarEnabled,
open,
close,
toggle,
}
}
export function useCloseSidebarOnEscape(
isOpen: Ref<boolean>,
close: () => void
) {
let triggerElement: HTMLButtonElement | undefined
watchEffect(() => {
triggerElement = isOpen.value
? (document.activeElement as HTMLButtonElement)
: undefined
})
onMounted(() => {
window.addEventListener('keyup', onEscape)
})
onUnmounted(() => {
window.removeEventListener('keyup', onEscape)
})
function onEscape(e: KeyboardEvent) {
if (e.key === 'Escape' && isOpen.value) {
close()
triggerElement?.focus()
}
}
}
export function useSidebarControl(item: ComputedRef<NotesSidebarItem>) {
const page = usePageData<PageData>()
const collapsed = ref(false)
const collapsible = computed(() => {
return item.value.collapsed != null
})
const isLink = computed(() => {
return !!item.value.link
})
const isActiveLink = computed(() => {
return isActive(page.value.path, item.value.link)
})
const hasActiveLink = computed(() => {
if (isActiveLink.value) {
return true
}
return item.value.items
? containsActiveLink(
page.value.path,
item.value.items as NotesSidebarItem[]
)
: false
})
const hasChildren = computed(() => {
return !!(item.value.items && item.value.items.length)
})
watchEffect(() => {
collapsed.value = !!(collapsible.value && item.value.collapsed)
})
watchEffect(() => {
;(isActiveLink.value || hasActiveLink.value) && (collapsed.value = false)
})
function toggle() {
if (collapsible.value) {
collapsed.value = !collapsed.value
}
}
return {
collapsed,
collapsible,
isLink,
isActiveLink,
hasActiveLink,
hasChildren,
toggle,
}
}
export function containsActiveLink(
path: string,
items: NotesSidebarItem | NotesSidebarItem[]
): boolean {
if (Array.isArray(items)) {
return items.some((item) => containsActiveLink(path, item))
}
return isActive(path, items.link)
? true
: items.items
? containsActiveLink(path, items.items as NotesSidebarItem[])
: false
}

View File

@ -2,3 +2,4 @@
@use 'fonts';
@use 'normalize';
@use 'nprogress';
@use 'utils';

View File

@ -0,0 +1,9 @@
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
white-space: nowrap;
clip: rect(0 0 0 0);
clip-path: inset(50%);
overflow: hidden;
}

View File

@ -0,0 +1,3 @@
export * from './shared.js'
export * from './normalizeLink.js'
export * from './socialIcons.js'

View File

@ -0,0 +1,20 @@
import { withBase } from '@vuepress/client'
import { isExternal, PATHNAME_PROTOCOL_RE } from './shared.js'
export function normalizeLink(url: string): string {
if (isExternal(url)) {
return url.replace(PATHNAME_PROTOCOL_RE, '')
}
const { pathname, search, hash } = new URL(url, 'http://example.com')
const normalizedPath =
pathname.endsWith('/') || pathname.endsWith('.html')
? url
: url.replace(
/(?:(^\.+)\/)?.*$/,
`$1${pathname.replace(/(\.md)?$/, '.html')}${search}${hash}`
)
return withBase(normalizedPath)
}

View File

@ -0,0 +1,43 @@
export const EXTERNAL_URL_RE = /^[a-z]+:/i
export const PATHNAME_PROTOCOL_RE = /^pathname:\/\//
export const APPEARANCE_KEY = 'vuepress-theme-appearance'
export const HASH_RE = /#.*$/
export const EXT_RE = /(index)?\.(md|html)$/
export const inBrowser = typeof document !== 'undefined'
export function isActive(
currentPath: string,
matchPath?: string,
asRegex = false
): boolean {
if (matchPath === undefined) {
return false
}
currentPath = normalize(`/${currentPath}`)
if (asRegex) {
return new RegExp(matchPath).test(currentPath)
}
if (normalize(matchPath) !== currentPath) {
return false
}
const hashMatch = matchPath.match(HASH_RE)
if (hashMatch) {
return (inBrowser ? location.hash : '') === hashMatch[0]
}
return true
}
export function normalize(path: string): string {
return decodeURI(path).replace(HASH_RE, '').replace(EXT_RE, '')
}
export function isExternal(path: string): boolean {
return EXTERNAL_URL_RE.test(path)
}

View File

@ -0,0 +1,22 @@
// Used under CC0 1.0 from https://simpleicons.org/
export const icons = {
discord:
'<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Discord</title><path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"/></svg>',
facebook:
'<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Facebook</title><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>',
github:
'<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>GitHub</title><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>',
instagram:
'<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Instagram</title><path d="M12 0C8.74 0 8.333.015 7.053.072 5.775.132 4.905.333 4.14.63c-.789.306-1.459.717-2.126 1.384S.935 3.35.63 4.14C.333 4.905.131 5.775.072 7.053.012 8.333 0 8.74 0 12s.015 3.667.072 4.947c.06 1.277.261 2.148.558 2.913.306.788.717 1.459 1.384 2.126.667.666 1.336 1.079 2.126 1.384.766.296 1.636.499 2.913.558C8.333 23.988 8.74 24 12 24s3.667-.015 4.947-.072c1.277-.06 2.148-.262 2.913-.558.788-.306 1.459-.718 2.126-1.384.666-.667 1.079-1.335 1.384-2.126.296-.765.499-1.636.558-2.913.06-1.28.072-1.687.072-4.947s-.015-3.667-.072-4.947c-.06-1.277-.262-2.149-.558-2.913-.306-.789-.718-1.459-1.384-2.126C21.319 1.347 20.651.935 19.86.63c-.765-.297-1.636-.499-2.913-.558C15.667.012 15.26 0 12 0zm0 2.16c3.203 0 3.585.016 4.85.071 1.17.055 1.805.249 2.227.415.562.217.96.477 1.382.896.419.42.679.819.896 1.381.164.422.36 1.057.413 2.227.057 1.266.07 1.646.07 4.85s-.015 3.585-.074 4.85c-.061 1.17-.256 1.805-.421 2.227-.224.562-.479.96-.899 1.382-.419.419-.824.679-1.38.896-.42.164-1.065.36-2.235.413-1.274.057-1.649.07-4.859.07-3.211 0-3.586-.015-4.859-.074-1.171-.061-1.816-.256-2.236-.421-.569-.224-.96-.479-1.379-.899-.421-.419-.69-.824-.9-1.38-.165-.42-.359-1.065-.42-2.235-.045-1.26-.061-1.649-.061-4.844 0-3.196.016-3.586.061-4.861.061-1.17.255-1.814.42-2.234.21-.57.479-.96.9-1.381.419-.419.81-.689 1.379-.898.42-.166 1.051-.361 2.221-.421 1.275-.045 1.65-.06 4.859-.06l.045.03zm0 3.678c-3.405 0-6.162 2.76-6.162 6.162 0 3.405 2.76 6.162 6.162 6.162 3.405 0 6.162-2.76 6.162-6.162 0-3.405-2.76-6.162-6.162-6.162zM12 16c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4-1.79 4-4 4zm7.846-10.405c0 .795-.646 1.44-1.44 1.44-.795 0-1.44-.646-1.44-1.44 0-.794.646-1.439 1.44-1.439.793-.001 1.44.645 1.44 1.439z"/></svg>',
linkedin:
'<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>LinkedIn</title><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>',
mastodon:
'<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Mastodon</title><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z"/></svg>',
slack:
'<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Slack</title><path d="M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zM6.313 15.165a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zM8.834 6.313a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312zM18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zM17.688 8.834a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312zM15.165 18.956a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zM15.165 17.688a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z"/></svg>',
twitter:
'<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Twitter</title><path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"/></svg>',
youtube:
'<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>YouTube</title><path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg>',
} as const

View File

@ -34,7 +34,14 @@ export const setupPlugins = (
return [
palettePlugin({ preset: 'sass' }),
themeDataPlugin({ themeData: localeOptions }),
themeDataPlugin({
themeData: {
...localeOptions,
notes: localeOptions.notes
? { dir: localeOptions.notes.dir, link: localeOptions.notes.link }
: undefined,
} as any,
}),
autoFrontmatterPlugin(autoFrontmatter(app, localeOptions)),
blogDataPlugin({
include: ['**/*.md'],

View File

@ -47,3 +47,4 @@ export type PlumeThemeData = ThemeData<PlumeThemeLocaleData>
export * from './locale.js'
export * from './plugins.js'
export * from './navbar.js'

View File

@ -1,12 +1,9 @@
import type { NotesDataOptions } from '@vuepress-plume/vuepress-plugin-notes-data'
import type { LocaleData } from '@vuepress/core'
import type { NavItem, NavItemWithLink } from './navbar.js'
// import type { NavbarConfig, NavLink } from '../layout/index.js'
// import type { PlumeThemeNotesOptions } from './notes.js'
// todo type
type NavbarConfig = any
type NavLink = any
export interface PlumeThemeAvatar {
/**
*
@ -22,46 +19,28 @@ export interface PlumeThemeAvatar {
description?: string
}
export interface PlumeThemeSocialOption {
/**
*
*/
email?: string
/**
* github链接 organization / Repositories
*/
github?: string
/**
*
*/
weiBo?: string
/**
*
*/
zhiHu?: string
/**
* QQ
*/
QQ?: string
/**
* facebook
*/
facebook?: string
/**
* twitter
*/
twitter?: string
/**
* linkedin
*/
linkedin?: string
export interface SocialLink {
icon: SocialLinkIcon
link: string
}
export type SocialLinkIcon =
| 'discord'
| 'facebook'
| 'github'
| 'instagram'
| 'linkedin'
| 'mastodon'
| 'slack'
| 'twitter'
| 'youtube'
| { svg: string }
export interface PlumeThemeLocaleData extends LocaleData {
/**
*
*/
home?: false | NavLink
home?: false | NavItemWithLink
/**
* logo
*/
@ -73,13 +52,7 @@ export interface PlumeThemeLocaleData extends LocaleData {
/**
*
*/
colorModeSwitch?: boolean
colorMode?: 'auto' | 'light' | 'dark'
toggleDarkMode?: string
toggleSidebar?: string
appearance?: boolean | 'dark'
/**
*
@ -98,7 +71,7 @@ export interface PlumeThemeLocaleData extends LocaleData {
/**
*
*/
social?: PlumeThemeSocialOption
social?: SocialLink[]
/**
*
@ -112,14 +85,14 @@ export interface PlumeThemeLocaleData extends LocaleData {
*
* @def{ text: '标签', link: '/tag/' }
*/
tag?: false | NavLink
tag?: false | NavItemWithLink
/**
* navbar配置
*
* @default { text: '分类', link: '/category/ }
*/
category?: false | NavLink
category?: false | NavItemWithLink
/**
* navbar
@ -128,7 +101,7 @@ export interface PlumeThemeLocaleData extends LocaleData {
*
* @default { text: '归档', link: '/timeline/' }
*/
archive?: false | NavLink
archive?: false | NavItemWithLink
/**
*
@ -165,7 +138,7 @@ export interface PlumeThemeLocaleData extends LocaleData {
*
* Set to `false` to disable navbar in current locale
*/
navbar?: false | NavbarConfig
navbar?: false | NavItem[]
/**
*
*/

View File

@ -0,0 +1,28 @@
export type NavItem = NavItemWithLink | NavItemWithChildren
export type NavItemWithLink = {
text: string
link: string
/**
* `activeMatch` is expected to be a regex string. We can't use actual
* RegExp object here because it isn't serializable
*/
activeMatch?: string
}
export type NavItemChildren = {
text?: string
items: NavItemWithLink[]
}
export interface NavItemWithChildren {
text?: string
items: (NavItemChildren | NavItemWithLink)[]
/**
* `activeMatch` is expected to be a regex string. We can't use actual
* RegExp object here because it isn't serializable
*/
activeMatch?: string
}