diff --git a/docs/.vuepress/config.ts b/docs/.vuepress/config.ts index fc75ef62..1286668a 100644 --- a/docs/.vuepress/config.ts +++ b/docs/.vuepress/config.ts @@ -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', diff --git a/packages/plugin-notes-data/src/node/prepareNotesData.ts b/packages/plugin-notes-data/src/node/prepareNotesData.ts index dd3e5b71..6795c729 100644 --- a/packages/plugin-notes-data/src/node/prepareNotesData.ts +++ b/packages/plugin-notes-data/src/node/prepareNotesData.ts @@ -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 || '', diff --git a/packages/plugin-notes-data/src/shared/index.ts b/packages/plugin-notes-data/src/shared/index.ts index 67d5d9d0..ea4a16fc 100644 --- a/packages/plugin-notes-data/src/shared/index.ts +++ b/packages/plugin-notes-data/src/shared/index.ts @@ -19,6 +19,7 @@ export type NotesSidebarItem = { text?: string link?: string dir?: string + collapsed?: boolean items?: NotesSidebar } diff --git a/packages/theme/src/client/components/AutoLink.vue b/packages/theme/src/client/components/AutoLink.vue new file mode 100644 index 00000000..ee8c11bc --- /dev/null +++ b/packages/theme/src/client/components/AutoLink.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/packages/theme/src/client/components/Flyout/MenuGroup.vue b/packages/theme/src/client/components/Flyout/MenuGroup.vue new file mode 100644 index 00000000..dc12f21e --- /dev/null +++ b/packages/theme/src/client/components/Flyout/MenuGroup.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/packages/theme/src/client/components/Flyout/MenuLink.vue b/packages/theme/src/client/components/Flyout/MenuLink.vue new file mode 100644 index 00000000..aa43cf43 --- /dev/null +++ b/packages/theme/src/client/components/Flyout/MenuLink.vue @@ -0,0 +1,57 @@ + + + + + diff --git a/packages/theme/src/client/components/Flyout/VMenu.vue b/packages/theme/src/client/components/Flyout/VMenu.vue new file mode 100644 index 00000000..91c3f491 --- /dev/null +++ b/packages/theme/src/client/components/Flyout/VMenu.vue @@ -0,0 +1,72 @@ + + + + + diff --git a/packages/theme/src/client/components/Flyout/index.vue b/packages/theme/src/client/components/Flyout/index.vue new file mode 100644 index 00000000..f4456911 --- /dev/null +++ b/packages/theme/src/client/components/Flyout/index.vue @@ -0,0 +1,138 @@ + + + + + diff --git a/packages/theme/src/client/components/Nav/NavBar.vue b/packages/theme/src/client/components/Nav/NavBar.vue index 9b6aed79..4b26fb1c 100644 --- a/packages/theme/src/client/components/Nav/NavBar.vue +++ b/packages/theme/src/client/components/Nav/NavBar.vue @@ -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(() => ({
+ + +
diff --git a/packages/theme/src/client/components/Nav/NavBarAppearance.vue b/packages/theme/src/client/components/Nav/NavBarAppearance.vue new file mode 100644 index 00000000..b04ed1fd --- /dev/null +++ b/packages/theme/src/client/components/Nav/NavBarAppearance.vue @@ -0,0 +1,25 @@ + + + + + diff --git a/packages/theme/src/client/components/Nav/NavBarExtra.vue b/packages/theme/src/client/components/Nav/NavBarExtra.vue new file mode 100644 index 00000000..fc8f22a7 --- /dev/null +++ b/packages/theme/src/client/components/Nav/NavBarExtra.vue @@ -0,0 +1,78 @@ + + + + + diff --git a/packages/theme/src/client/components/Nav/NavBarHamburger.vue b/packages/theme/src/client/components/Nav/NavBarHamburger.vue new file mode 100644 index 00000000..f4129e2a --- /dev/null +++ b/packages/theme/src/client/components/Nav/NavBarHamburger.vue @@ -0,0 +1,118 @@ + + + + + diff --git a/packages/theme/src/client/components/Nav/NavBarMenu.vue b/packages/theme/src/client/components/Nav/NavBarMenu.vue new file mode 100644 index 00000000..10c45b53 --- /dev/null +++ b/packages/theme/src/client/components/Nav/NavBarMenu.vue @@ -0,0 +1,35 @@ + + + + + diff --git a/packages/theme/src/client/components/Nav/NavBarMenuGroup.vue b/packages/theme/src/client/components/Nav/NavBarMenuGroup.vue new file mode 100644 index 00000000..deb023ee --- /dev/null +++ b/packages/theme/src/client/components/Nav/NavBarMenuGroup.vue @@ -0,0 +1,23 @@ + + + diff --git a/packages/theme/src/client/components/Nav/NavBarMenuLink.vue b/packages/theme/src/client/components/Nav/NavBarMenuLink.vue new file mode 100644 index 00000000..0b5bd0a5 --- /dev/null +++ b/packages/theme/src/client/components/Nav/NavBarMenuLink.vue @@ -0,0 +1,50 @@ + + + + + diff --git a/packages/theme/src/client/components/Nav/NavBarSocialLinks.vue b/packages/theme/src/client/components/Nav/NavBarSocialLinks.vue new file mode 100644 index 00000000..9f1f494c --- /dev/null +++ b/packages/theme/src/client/components/Nav/NavBarSocialLinks.vue @@ -0,0 +1,27 @@ + + + + + diff --git a/packages/theme/src/client/components/SocialLink.vue b/packages/theme/src/client/components/SocialLink.vue new file mode 100644 index 00000000..687ac3fe --- /dev/null +++ b/packages/theme/src/client/components/SocialLink.vue @@ -0,0 +1,50 @@ + + + + + diff --git a/packages/theme/src/client/components/SocialLinks.vue b/packages/theme/src/client/components/SocialLinks.vue new file mode 100644 index 00000000..dcbee7a0 --- /dev/null +++ b/packages/theme/src/client/components/SocialLinks.vue @@ -0,0 +1,27 @@ + + + + + diff --git a/packages/theme/src/client/components/Switch.vue b/packages/theme/src/client/components/Switch.vue new file mode 100644 index 00000000..174313fc --- /dev/null +++ b/packages/theme/src/client/components/Switch.vue @@ -0,0 +1,63 @@ + + + diff --git a/packages/theme/src/client/components/SwitchAppearance.vue b/packages/theme/src/client/components/SwitchAppearance.vue new file mode 100644 index 00000000..d496fb24 --- /dev/null +++ b/packages/theme/src/client/components/SwitchAppearance.vue @@ -0,0 +1,118 @@ + + + + + diff --git a/packages/theme/src/client/composables/darkMode.ts b/packages/theme/src/client/composables/darkMode.ts index 2e76b835..4a190911 100644 --- a/packages/theme/src/client/composables/darkMode.ts +++ b/packages/theme/src/client/composables/darkMode.ts @@ -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 @@ -13,60 +11,17 @@ export const darkModeSymbol: InjectionKey = 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({ - 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 `` element - const htmlEl = window?.document.querySelector('html') - htmlEl?.classList.toggle('dark', value) - } - - onMounted(() => { - watch(isDarkMode, update, { immediate: true }) - }) - - onUnmounted(() => update()) + const isDark = ref(false) + provide(darkModeSymbol, isDark) } diff --git a/packages/theme/src/client/composables/flyout.ts b/packages/theme/src/client/composables/flyout.ts new file mode 100644 index 00000000..cecc2520 --- /dev/null +++ b/packages/theme/src/client/composables/flyout.ts @@ -0,0 +1,59 @@ +import { onUnmounted, readonly, ref, type Ref, watch } from 'vue' +import { inBrowser } from '../utils/index.js' + +interface UseFlyoutOptions { + el: Ref + onFocus?(): void + onBlur?(): void +} + +export const focusedElement = ref() + +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 +} diff --git a/packages/theme/src/client/composables/sidebar.ts b/packages/theme/src/client/composables/sidebar.ts index ac3e0a3b..85738c51 100644 --- a/packages/theme/src/client/composables/sidebar.ts +++ b/packages/theme/src/client/composables/sidebar.ts @@ -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, + 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) { + const page = usePageData() + + 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 +} diff --git a/packages/theme/src/client/styles/index.scss b/packages/theme/src/client/styles/index.scss index dec863da..14cd491e 100644 --- a/packages/theme/src/client/styles/index.scss +++ b/packages/theme/src/client/styles/index.scss @@ -2,3 +2,4 @@ @use 'fonts'; @use 'normalize'; @use 'nprogress'; +@use 'utils'; diff --git a/packages/theme/src/client/styles/utils.scss b/packages/theme/src/client/styles/utils.scss new file mode 100644 index 00000000..65c7e55e --- /dev/null +++ b/packages/theme/src/client/styles/utils.scss @@ -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; +} diff --git a/packages/theme/src/client/utils/index.ts b/packages/theme/src/client/utils/index.ts new file mode 100644 index 00000000..326882af --- /dev/null +++ b/packages/theme/src/client/utils/index.ts @@ -0,0 +1,3 @@ +export * from './shared.js' +export * from './normalizeLink.js' +export * from './socialIcons.js' diff --git a/packages/theme/src/client/utils/normalizeLink.ts b/packages/theme/src/client/utils/normalizeLink.ts new file mode 100644 index 00000000..cddb372e --- /dev/null +++ b/packages/theme/src/client/utils/normalizeLink.ts @@ -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) +} diff --git a/packages/theme/src/client/utils/shared.ts b/packages/theme/src/client/utils/shared.ts new file mode 100644 index 00000000..e0a49ff9 --- /dev/null +++ b/packages/theme/src/client/utils/shared.ts @@ -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) +} diff --git a/packages/theme/src/client/utils/socialIcons.ts b/packages/theme/src/client/utils/socialIcons.ts new file mode 100644 index 00000000..54a91e6e --- /dev/null +++ b/packages/theme/src/client/utils/socialIcons.ts @@ -0,0 +1,22 @@ +// Used under CC0 1.0 from https://simpleicons.org/ + +export const icons = { + discord: + 'Discord', + facebook: + 'Facebook', + github: + 'GitHub', + instagram: + 'Instagram', + linkedin: + 'LinkedIn', + mastodon: + 'Mastodon', + slack: + 'Slack', + twitter: + 'Twitter', + youtube: + 'YouTube', +} as const diff --git a/packages/theme/src/node/plugins.ts b/packages/theme/src/node/plugins.ts index c2f17666..dff0f715 100644 --- a/packages/theme/src/node/plugins.ts +++ b/packages/theme/src/node/plugins.ts @@ -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'], diff --git a/packages/theme/src/shared/options/index.ts b/packages/theme/src/shared/options/index.ts index 3363b0e1..4bba3d83 100644 --- a/packages/theme/src/shared/options/index.ts +++ b/packages/theme/src/shared/options/index.ts @@ -47,3 +47,4 @@ export type PlumeThemeData = ThemeData export * from './locale.js' export * from './plugins.js' +export * from './navbar.js' diff --git a/packages/theme/src/shared/options/locale.ts b/packages/theme/src/shared/options/locale.ts index 3183df1a..ef93cf02 100644 --- a/packages/theme/src/shared/options/locale.ts +++ b/packages/theme/src/shared/options/locale.ts @@ -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[] /** * 外部链接打开方式 */ diff --git a/packages/theme/src/shared/options/navbar.ts b/packages/theme/src/shared/options/navbar.ts new file mode 100644 index 00000000..b70ed4a8 --- /dev/null +++ b/packages/theme/src/shared/options/navbar.ts @@ -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 +}