feat(theme): add page components

This commit is contained in:
pengzhanbo 2023-02-12 06:03:52 +08:00
parent 4e98011b65
commit 764c58693e
28 changed files with 1832 additions and 225 deletions

View File

@ -52,6 +52,7 @@
"@vuepress/plugin-palette": "2.0.0-beta.60",
"@vuepress/plugin-prismjs": "2.0.0-beta.60",
"@vuepress/plugin-search": "2.0.0-beta.60",
"@vuepress/plugin-shiki": "2.0.0-beta.60",
"@vuepress/plugin-theme-data": "2.0.0-beta.60",
"@vuepress/plugin-toc": "2.0.0-beta.60",
"@vuepress/shared": "2.0.0-beta.60",

View File

@ -0,0 +1,28 @@
<script lang="ts" setup>
import Page from './Page.vue'
</script>
<template>
<div class="plume-blog-page">
<Page />
</div>
</template>
<style scoped>
.plume-blog-page {
flex-grow: 1;
flex-shrink: 0;
margin: var(--vp-layout-top-height, 0px) auto 0;
width: 100%;
}
@media (min-width: 960px) {
.plume-blog-page {
padding-top: var(--vp-nav-height);
}
.plume-blog-page.has-sidebar {
margin: var(--vp-layout-top-height, 0px) 0 0;
padding-left: var(--vp-sidebar-width);
}
}
</style>

View File

@ -0,0 +1,48 @@
<script lang="ts" setup>
import { useSidebar } from '../composables/index.js'
const { hasSidebar } = useSidebar()
</script>
<template>
<div class="layout-content" :class="{ 'has-sidebar': hasSidebar }">
<slot />
</div>
</template>
<style scoped>
.layout-content {
flex-grow: 1;
flex-shrink: 0;
margin: var(--vp-layout-top-height, 0px) auto 0;
width: 100%;
}
.layout-content.is-home {
width: 100%;
max-width: 100%;
}
.layout-content.has-sidebar {
margin: 0;
}
@media (min-width: 960px) {
.layout-content {
padding-top: var(--vp-nav-height);
}
.layout-content.has-sidebar {
margin: var(--vp-layout-top-height, 0px) 0 0;
padding-left: var(--vp-sidebar-width);
}
}
@media (min-width: 1440px) {
.layout-content.has-sidebar {
padding-right: calc((100vw - var(--vp-layout-max-width)) / 2);
padding-left: calc(
(100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width)
);
}
}
</style>

View File

@ -1,22 +1,25 @@
<script lang="ts" setup>
import { useSiteLocaleData } from '@vuepress/client'
import { useSidebar } from '../../composables/index.js'
import { useThemeLocaleData } from '../../composables/themeData.js'
import AutoLink from '../AutoLink.vue'
import VImage from '../VImage.vue'
const theme = useThemeLocaleData()
const site = useSiteLocaleData()
const { hasSidebar } = useSidebar()
</script>
<template>
<div class="navbar-title">
<a class="title" :href="theme.home">
<div class="navbar-title" :class="{ 'has-sidebar': hasSidebar }">
<AutoLink class="title" :href="theme.home ? theme.home.link : ''">
<VImage
v-if="theme.logo"
class="logo"
:image="{ light: theme.logo, dark: theme.logoDark || '' }"
/>
{{ site.title }}
</a>
</AutoLink>
</div>
</template>

View File

@ -1,11 +1,164 @@
<script lang="ts" setup>
import { useSidebar } from '../composables/index.js'
import PageAside from './PageAside.vue'
const { hasSidebar, hasAside } = useSidebar()
</script>
<template>
<div class="plume-page">
<Content class="plume-content" />
<div
class="plume-page"
:class="{ 'has-sidebar': hasSidebar, 'has-aside': hasAside }"
>
<div class="container">
<div v-if="hasAside" class="aside">
<div class="aside-curtain" />
<div class="aside-container">
<div class="aside-content">
<PageAside />
</div>
</div>
</div>
<div class="content">
<div class="content-container">
<main class="main">
<Content class="plume-content" />
</main>
</div>
</div>
</div>
</div>
</template>
<style>
<style scoped>
.plume-page {
position: relative;
display: flex;
}
.plume-page {
padding: 32px 24px 96px;
width: 100%;
}
@media (min-width: 768px) {
.plume-page {
padding: 48px 32px 128px;
}
}
@media (min-width: 960px) {
.plume-page {
padding: 32px 32px 0;
}
.plume-page:not(.has-sidebar) .container {
display: flex;
justify-content: center;
max-width: 992px;
}
.plume-page:not(.has-sidebar) .content {
max-width: 752px;
}
}
@media (min-width: 1280px) {
.plume-page .container {
display: flex;
justify-content: center;
}
.plume-page .aside {
display: block;
}
}
@media (min-width: 1440px) {
.plume-page:not(.has-sidebar) .content {
max-width: 784px;
}
.plume-page:not(.has-sidebar) .container {
max-width: 1204px;
}
}
.container {
margin: 0 auto;
width: 100%;
}
.aside {
position: relative;
display: none;
order: 2;
flex-grow: 1;
padding-left: 32px;
width: 100%;
max-width: 256px;
}
.aside-container {
position: sticky;
top: 0;
margin-top: calc(
(var(--vp-nav-height) + var(--vp-layout-top-height, 0px)) * -1 - 32px
);
padding-top: calc(
var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 32px
);
height: 100vh;
overflow-x: hidden;
overflow-y: auto;
scrollbar-width: none;
}
.aside-container::-webkit-scrollbar {
display: none;
}
.aside-curtain {
position: fixed;
bottom: 0;
z-index: 10;
width: 224px;
height: 32px;
background: linear-gradient(transparent, var(--vp-c-bg) 70%);
}
.aside-content {
display: flex;
flex-direction: column;
min-height: calc(
100vh - (var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 32px)
);
padding-bottom: 32px;
}
.content {
position: relative;
margin: 0 auto;
width: 100%;
}
@media (min-width: 960px) {
.content {
padding: 0 32px 128px;
}
}
@media (min-width: 1280px) {
.content {
order: 1;
margin: 0;
min-width: 640px;
}
}
.content-container {
margin: 0 auto;
}
.plume-page.has-aside .content-container {
max-width: 688px;
}
</style>

View File

@ -0,0 +1,94 @@
<script lang="ts" setup>
import { usePageData } from '@vuepress/client'
import { computed, ref } from 'vue'
import { useActiveAnchor } from '../composables/aside.js'
import PageAsideItem from './PageAsideItem.vue'
const page = usePageData()
const headers = computed(() => page.value.headers)
const hasOutline = computed(() => headers.value.length > 0)
const container = ref()
const marker = ref()
useActiveAnchor(container, marker)
function handleClick({ target: el }: Event) {
const id = '#' + (el as HTMLAnchorElement).href!.split('#')[1]
const heading = document.querySelector<HTMLAnchorElement>(
decodeURIComponent(id)
)
heading?.focus()
}
</script>
<template>
<div class="page-aside">
<div
ref="container"
class="page-aside-outline"
:class="{ 'has-outline': hasOutline }"
>
<div class="content">
<div ref="marker" class="outline-marker" />
<div class="outline-title">On this page</div>
<nav aria-labelledby="doc-outline-aria-label">
<span id="doc-outline-aria-label" class="visually-hidden">
Table of Contents for current page
</span>
<PageAsideItem
:headers="headers"
:root="true"
:on-click="handleClick"
/>
</nav>
</div>
</div>
</div>
</template>
<style scoped>
.page-aside {
display: flex;
flex-direction: column;
flex-grow: 1;
}
.page-aside-outline {
display: none;
}
.page-aside-outline.has-outline {
display: block;
}
.content {
position: relative;
border-left: 1px solid var(--vp-c-divider);
padding-left: 16px;
font-size: 13px;
font-weight: 500;
}
.outline-marker {
position: absolute;
top: 32px;
left: -1px;
z-index: 0;
opacity: 0;
width: 1px;
height: 18px;
background-color: var(--vp-c-brand);
transition: top 0.25s cubic-bezier(0, 1, 0.5, 1), background-color 0.5s,
opacity 0.25s;
}
.outline-title {
letter-spacing: 0.4px;
line-height: 28px;
font-size: 13px;
font-weight: 600;
}
</style>

View File

@ -0,0 +1,51 @@
<script setup lang="ts">
import type { PageHeader } from '@vuepress/client'
defineProps<{
headers: PageHeader[]
onClick: (e: MouseEvent) => void
root?: boolean
}>()
</script>
<template>
<ul :class="root ? 'root' : 'nested'">
<li v-for="{ children, link, title } in headers" :key="link">
<a class="outline-link" :href="link" @click="onClick">{{ title }}</a>
<template v-if="children?.length">
<PageAsideItem :headers="children" :on-click="onClick" />
</template>
</li>
</ul>
</template>
<style scoped>
.root {
position: relative;
z-index: 1;
}
.nested {
padding-left: 13px;
}
.outline-link {
display: block;
line-height: 28px;
color: var(--vp-c-text-2);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: color 0.5s;
}
.outline-link:hover,
.outline-link.active {
color: var(--vp-c-text-1);
transition: color 0.25s;
}
.outline-link.nested {
padding-left: 13px;
}
</style>

View File

@ -0,0 +1,148 @@
<script lang="ts" setup>
import { clearAllBodyScrollLocks, disableBodyScroll } from 'body-scroll-lock'
import { ref, watchPostEffect } from 'vue'
import { useSidebar } from '../composables/sidebar.js'
import SidebarItem from './SidebarItem.vue'
const { sidebarGroups, hasSidebar } = useSidebar()
const props = defineProps<{
open: boolean
}>()
// a11y: focus Nav element when menu has opened
const navEl = ref<HTMLElement | null>(null)
function lockBodyScroll() {
disableBodyScroll(navEl.value!, { reserveScrollBarGap: true })
}
function unlockBodyScroll() {
clearAllBodyScrollLocks()
}
watchPostEffect(async () => {
if (props.open) {
lockBodyScroll()
navEl.value?.focus()
} else {
unlockBodyScroll()
}
})
</script>
<template>
<aside
v-if="hasSidebar"
ref="navEl"
class="sidebar-wrapper"
:class="{ open }"
@click.stop
>
<div class="curtain" />
<nav
id="SidebarNav"
class="nav"
aria-labelledby="sidebar-aria-label"
tabindex="-1"
>
<span id="sidebar-aria-label" class="visually-hidden">
Sidebar Navigation
</span>
<div v-for="item in sidebarGroups" :key="item.text" class="group">
<SidebarItem :item="item" :depth="0" />
</div>
</nav>
</aside>
</template>
<style scoped>
.sidebar-wrapper {
position: fixed;
top: var(--vp-layout-top-height, 0px);
bottom: 0;
left: 0;
z-index: var(--vp-z-index-sidebar);
padding: 32px 32px 96px;
width: calc(100vw - 64px);
max-width: 320px;
background-color: var(--vp-sidebar-bg-color);
opacity: 0;
box-shadow: var(--vp-c-shadow-3);
overflow-x: hidden;
overflow-y: auto;
transform: translateX(-100%);
transition: opacity 0.5s, transform 0.25s ease;
}
.sidebar-wrapper.open {
opacity: 1;
visibility: visible;
transform: translateX(0);
transition: opacity 0.25s, transform 0.5s cubic-bezier(0.19, 1, 0.22, 1);
}
.dark .sidebar-wrapper {
box-shadow: var(--vp-shadow-1);
}
@media (min-width: 960px) {
.sidebar-wrapper {
z-index: 1;
padding-top: var(--vp-nav-height);
padding-bottom: 128px;
width: var(--vp-sidebar-width);
max-width: 100%;
background-color: var(--vp-sidebar-bg-color);
opacity: 1;
visibility: visible;
box-shadow: none;
transform: translateX(0);
}
}
@media (min-width: 1440px) {
.sidebar-wrapper {
padding-left: max(
32px,
calc((100% - (var(--vp-layout-max-width) - 64px)) / 2)
);
width: calc(
(100% - (var(--vp-layout-max-width) - 64px)) / 2 + var(--vp-sidebar-width) -
32px
);
}
}
@media (min-width: 960px) {
.curtain {
position: sticky;
top: -64px;
left: 0;
z-index: 1;
margin-top: calc(var(--vp-nav-height) * -1);
margin-right: -32px;
margin-left: -32px;
height: var(--vp-nav-height);
background-color: var(--vp-sidebar-bg-color);
}
}
.nav {
outline: 0;
}
.group + .group {
border-top: 1px solid var(--vp-c-divider);
padding-top: 10px;
}
@media (min-width: 960px) {
.group {
padding-top: 10px;
width: calc(var(--vp-sidebar-width) - 64px);
}
}
</style>

View File

@ -0,0 +1,223 @@
<script setup lang="ts">
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
import type { NotesSidebarItem } from '@vuepress-plume/vuepress-plugin-notes-data'
import { computed } from 'vue'
import { useSidebarControl } from '../composables/sidebar.js'
import AutoLink from './AutoLink.vue'
import IconChevronRight from './icons/IconChevronRight.vue'
const props = defineProps<{
item: NotesSidebarItem
depth: number
}>()
const {
collapsed,
collapsible,
isLink,
isActiveLink,
hasActiveLink,
hasChildren,
toggle,
} = useSidebarControl(computed(() => props.item))
const sectionTag = computed(() => (hasChildren.value ? 'section' : `div`))
const linkTag = computed(() => (isLink.value ? 'a' : 'div'))
const textTag = computed(() => {
return !hasChildren.value
? 'p'
: props.depth + 2 === 7
? 'p'
: `h${props.depth + 2}`
})
const itemRole = computed(() => (isLink.value ? undefined : 'button'))
const classes = computed(() => [
[`level-${props.depth}`],
{ collapsible: collapsible.value },
{ collapsed: collapsed.value },
{ 'is-link': isLink.value },
{ 'is-active': isActiveLink.value },
{ 'has-active': hasActiveLink.value },
])
function onItemClick() {
!props.item.link && toggle()
}
function onCaretClick() {
props.item.link && toggle()
}
</script>
<template>
<Component :is="sectionTag" class="sidebar-item" :class="classes">
<div v-if="item.text" class="item" :role="itemRole" @click="onItemClick">
<div class="indicator" />
<AutoLink :tag="linkTag" class="link" :href="item.link">
<!-- eslint-disable vue/no-v-text-v-html-on-component -->
<!-- eslint-disable-next-line vue/no-v-html -->
<Component :is="textTag" class="text" v-html="item.text" />
</AutoLink>
<div
v-if="item.collapsed != null"
class="caret"
role="button"
@click="onCaretClick"
>
<IconChevronRight class="caret-icon" />
</div>
</div>
<div v-if="item.items && item.items.length" class="items">
<template v-if="depth < 5">
<SidebarItem
v-for="i in (item.items as NotesSidebarItem[])"
:key="i.text"
:item="i"
:depth="depth + 1"
/>
</template>
</div>
</Component>
</template>
<style scoped>
.sidebar-item.level-0 {
padding-bottom: 24px;
}
.sidebar-item.collapsed.level-0 {
padding-bottom: 10px;
}
.item {
position: relative;
display: flex;
width: 100%;
}
.sidebar-item.collapsible > .item {
cursor: pointer;
}
.indicator {
position: absolute;
top: 6px;
bottom: 6px;
left: -17px;
width: 1px;
transition: background-color 0.25s;
}
.sidebar-item.level-2.is-active > .item > .indicator,
.sidebar-item.level-3.is-active > .item > .indicator,
.sidebar-item.level-4.is-active > .item > .indicator,
.sidebar-item.level-5.is-active > .item > .indicator {
background-color: var(--vp-c-brand);
}
.link {
display: block;
flex-grow: 1;
}
.text {
flex-grow: 1;
padding: 4px 0;
line-height: 24px;
font-size: 14px;
transition: color 0.25s;
}
.sidebar-item.level-0 .text {
font-weight: 700;
color: var(--vp-c-text-1);
}
.sidebar-item.level-1 .text,
.sidebar-item.level-2 .text,
.sidebar-item.level-3 .text,
.sidebar-item.level-4 .text,
.sidebar-item.level-5 .text {
font-weight: 500;
color: var(--vp-c-text-2);
}
.sidebar-item.level-0.is-link > .item > .link:hover .text,
.sidebar-item.level-1.is-link > .item > .link:hover .text,
.sidebar-item.level-2.is-link > .item > .link:hover .text,
.sidebar-item.level-3.is-link > .item > .link:hover .text,
.sidebar-item.level-4.is-link > .item > .link:hover .text,
.sidebar-item.level-5.is-link > .item > .link:hover .text {
color: var(--vp-c-brand);
}
.sidebar-item.level-0.has-active > .item > .link > .text,
.sidebar-item.level-1.has-active > .item > .link > .text,
.sidebar-item.level-2.has-active > .item > .link > .text,
.sidebar-item.level-3.has-active > .item > .link > .text,
.sidebar-item.level-4.has-active > .item > .link > .text,
.sidebar-item.level-5.has-active > .item > .link > .text {
color: var(--vp-c-text-1);
}
.sidebar-item.level-0.is-active > .item .link > .text,
.sidebar-item.level-1.is-active > .item .link > .text,
.sidebar-item.level-2.is-active > .item .link > .text,
.sidebar-item.level-3.is-active > .item .link > .text,
.sidebar-item.level-4.is-active > .item .link > .text,
.sidebar-item.level-5.is-active > .item .link > .text {
color: var(--vp-c-brand);
}
.caret {
display: flex;
justify-content: center;
align-items: center;
margin-right: -7px;
width: 32px;
height: 32px;
color: var(--vp-c-text-3);
cursor: pointer;
transition: color 0.25s;
}
.item:hover .caret {
color: var(--vp-c-text-2);
}
.item:hover .caret:hover {
color: var(--vp-c-text-1);
}
.caret-icon {
width: 18px;
height: 18px;
fill: currentColor;
transform: rotate(90deg);
transition: transform 0.25s;
}
.sidebar-item.collapsed .caret-icon {
transform: rotate(0);
}
.sidebar-item.level-1 .items,
.sidebar-item.level-2 .items,
.sidebar-item.level-3 .items,
.sidebar-item.level-4 .items,
.sidebar-item.level-5 .items {
border-left: 1px solid var(--vp-c-divider);
padding-left: 16px;
}
.sidebar-item.collapsed .items {
display: none;
}
</style>

View File

@ -0,0 +1,148 @@
import { usePageData } from '@vuepress/client'
import type { PageHeader } from '@vuepress/client'
import type { PropType, VNode } from 'vue'
import { computed, defineComponent, h, toRefs } from 'vue'
import type { RouteLocationNormalizedLoaded } from 'vue-router'
import { useRoute } from 'vue-router'
import { scrollTo } from '../utils/index.js'
export type TocPropsHeaders = PageHeader[]
export interface TocPropsOptions {
containerTag: string
containerClass: string
listClass: string
itemClass: string
linkClass: string
linkActiveClass: string
linkChildrenActiveClass: string
}
export interface TocProps {
headers: TocPropsHeaders
options: TocPropsOptions
}
const renderLink = (
header: PageHeader,
options: TocPropsOptions,
route: RouteLocationNormalizedLoaded
): VNode => {
const hash = `#${header.slug}`
const linkClass = [options.linkClass]
if (options.linkActiveClass && route.hash === hash) {
linkClass.push(options.linkActiveClass)
}
if (
options.linkChildrenActiveClass &&
header.children.some((item) => `#${item.slug}` === route.hash)
) {
linkClass.push(options.linkChildrenActiveClass)
}
const setActiveRouteHash = (): void => {
const headerAnchors: HTMLAnchorElement[] = Array.from(
document.querySelectorAll('.header-anchor')
)
const anchor = headerAnchors.find(
(anchor) => decodeURI(anchor.hash) === hash
)
if (!anchor) return
const el = document.documentElement
const top = anchor.getBoundingClientRect().top - 80 + el.scrollTop
scrollTo(document, top)
}
return h(
'a',
{
href: hash,
class: linkClass,
ariaLabel: header.title,
onClick: (e: MouseEvent) => {
e.preventDefault()
setActiveRouteHash()
},
},
header.title
)
}
const renderHeaders = (
headers: PageHeader[],
options: TocPropsOptions,
route: RouteLocationNormalizedLoaded
): VNode[] => {
if (headers.length === 0) {
return []
}
return [
h(
'ul',
{ class: options.listClass },
headers.map((header) =>
h('li', { class: options.itemClass }, [
renderLink(header, options, route),
renderHeaders(header.children, options, route),
])
)
),
]
}
const Toc = defineComponent({
name: 'Toc',
props: {
headers: {
type: Array as PropType<TocPropsHeaders>,
required: false,
default: null,
},
options: {
type: Object as PropType<TocPropsOptions>,
required: false,
default: () => ({}),
},
},
setup(props) {
const { headers: propsHeaders, options: propsOptions } = toRefs(props)
const defaultOptions: TocPropsOptions = {
containerTag: 'nav',
containerClass: 'theme-plume-toc',
listClass: 'theme-plume-toc-list',
itemClass: 'theme-plume-toc-item',
linkClass: 'theme-plume-toc-link',
linkActiveClass: 'active',
linkChildrenActiveClass: 'active',
}
const route = useRoute()
const page = usePageData()
const headers = computed<TocPropsHeaders>(() => {
const headerToUse = propsHeaders.value || page.value.headers
return headerToUse[0]?.level === 1 ? headerToUse[0].children : headerToUse
})
const options = computed<TocPropsOptions>(() => ({
...defaultOptions,
...propsOptions.value,
}))
return () => {
const renderedHeaders = renderHeaders(headers.value, options.value, route)
if (options.value.containerTag) {
return h(
options.value.containerTag,
{ class: options.value.containerClass },
renderedHeaders
)
}
return renderedHeaders
}
},
})
export default Toc

View File

@ -0,0 +1,140 @@
import { useMediaQuery } from '@vueuse/core'
import type { Ref } from 'vue'
import { computed, onMounted, onUnmounted, onUpdated } from 'vue'
import { throttleAndDebounce } from '../utils/index.js'
import { useSidebar } from './sidebar.js'
const PAGE_OFFSET = 71
export function useAside() {
const { hasSidebar } = useSidebar()
const is960 = useMediaQuery('(min-width: 960px)')
const is1280 = useMediaQuery('(min-width: 1280px)')
const isAsideEnabled = computed(() => {
if (!is1280.value && !is960.value) {
return false
}
return hasSidebar.value ? is1280.value : is960.value
})
return {
isAsideEnabled,
}
}
export function useActiveAnchor(
container: Ref<HTMLElement>,
marker: Ref<HTMLElement>
) {
const { isAsideEnabled } = useAside()
const onScroll = throttleAndDebounce(setActiveLink, 100)
let prevActiveLink: HTMLAnchorElement | null = null
onMounted(() => {
requestAnimationFrame(setActiveLink)
window.addEventListener('scroll', onScroll)
})
onUpdated(() => {
// sidebar update means a route change
activateLink(location.hash)
})
onUnmounted(() => {
window.removeEventListener('scroll', onScroll)
})
function setActiveLink() {
if (!isAsideEnabled.value) {
return
}
const links = [].slice.call(
container.value.querySelectorAll('.outline-link')
) as HTMLAnchorElement[]
const anchors = [].slice
.call(document.querySelectorAll('.content .header-anchor'))
.filter((anchor: HTMLAnchorElement) => {
return links.some((link) => {
return link.hash === anchor.hash && anchor.offsetParent !== null
})
}) as HTMLAnchorElement[]
const scrollY = window.scrollY
const innerHeight = window.innerHeight
const offsetHeight = document.body.offsetHeight
const isBottom = Math.abs(scrollY + innerHeight - offsetHeight) < 1
// page bottom - highlight last one
if (anchors.length && isBottom) {
activateLink(anchors[anchors.length - 1].hash)
return
}
for (let i = 0; i < anchors.length; i++) {
const anchor = anchors[i]
const nextAnchor = anchors[i + 1]
const [isActive, hash] = isAnchorActive(i, anchor, nextAnchor)
if (isActive) {
activateLink(hash)
return
}
}
}
function activateLink(hash: string | null) {
if (prevActiveLink) {
prevActiveLink.classList.remove('active')
}
if (hash !== null) {
prevActiveLink = container.value.querySelector(
`a[href="${decodeURIComponent(hash)}"]`
)
}
const activeLink = prevActiveLink
if (activeLink) {
activeLink.classList.add('active')
marker.value.style.top = activeLink.offsetTop + 33 + 'px'
marker.value.style.opacity = '1'
} else {
marker.value.style.top = '33px'
marker.value.style.opacity = '0'
}
}
}
function getAnchorTop(anchor: HTMLAnchorElement): number {
return anchor.parentElement!.offsetTop - PAGE_OFFSET
}
function isAnchorActive(
index: number,
anchor: HTMLAnchorElement,
nextAnchor: HTMLAnchorElement | undefined
): [boolean, string | null] {
const scrollTop = window.scrollY
if (index === 0 && scrollTop === 0) {
return [true, null]
}
if (scrollTop < getAnchorTop(anchor)) {
return [false, null]
}
if (!nextAnchor || scrollTop < getAnchorTop(nextAnchor)) {
return [true, anchor.hash]
}
return [false, null]
}

View File

@ -2,3 +2,5 @@ export * from './darkMode.js'
export * from './useScrollPromise.js'
export * from './themeData.js'
export * from './useResolveRouteWithRedirect.js'
export * from './sidebar.js'
export * from './aside.js'

View File

@ -9,6 +9,7 @@ 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 type { PlumeThemePageData } from '../../shared/index.js'
import { isActive } from '../utils/index.js'
import { useThemeLocaleData } from './themeData.js'
@ -24,6 +25,7 @@ export function useSidebar() {
const notesData = useNotesData()
const theme = useThemeLocaleData()
const frontmatter = usePageFrontmatter()
const page = usePageData<PlumeThemePageData>()
const is960 = useMediaQuery('(min-width: 960px)')
@ -33,7 +35,11 @@ export function useSidebar() {
return theme.value.notes ? getSidebarList(route.path, notesData.value) : []
})
const hasSidebar = computed(() => {
return !frontmatter.value.home && sidebar.value.length > 0
return (
!frontmatter.value.home &&
!page.value.isBlogPost &&
sidebar.value.length > 0
)
})
const hasAside = computed(() => {
@ -42,6 +48,10 @@ export function useSidebar() {
const isSidebarEnabled = computed(() => hasSidebar.value && is960.value)
const sidebarGroups = computed(() => {
return hasSidebar.value ? getSidebarGroups(sidebar.value) : []
})
function open() {
isOpen.value = true
}
@ -60,6 +70,7 @@ export function useSidebar() {
hasSidebar,
hasAside,
isSidebarEnabled,
sidebarGroups,
open,
close,
toggle,
@ -167,3 +178,31 @@ export function containsActiveLink(
? containsActiveLink(path, items.items as NotesSidebarItem[])
: false
}
/**
* Get or generate sidebar group from the given sidebar items.
*/
export function getSidebarGroups(
sidebar: NotesSidebarItem[]
): NotesSidebarItem[] {
const groups: NotesSidebarItem[] = []
let lastGroupIndex = 0
for (const index in sidebar) {
const item = sidebar[index]
if (item.items) {
lastGroupIndex = groups.push(item)
continue
}
if (!groups[lastGroupIndex]) {
groups.push({ items: [] })
}
groups[lastGroupIndex]!.items!.push(item)
}
return groups
}

View File

@ -1,7 +1,34 @@
<script setup lang="ts">
import { usePageData } from '@vuepress/client'
import { provide, watch } from 'vue'
import { useRoute } from 'vue-router'
import type { PlumeThemePageData } from '../../shared/index.js'
import LayoutContent from '../components/LayoutContent.vue'
import Nav from '../components/Nav/index.vue'
import Page from '../components/Page.vue'
import { useScrollPromise, useThemeLocaleData } from '../composables/index.js'
import Sidebar from '../components/Sidebar.vue'
import {
useCloseSidebarOnEscape,
useScrollPromise,
useSidebar,
useThemeLocaleData,
} from '../composables/index.js'
const page = usePageData<PlumeThemePageData>()
const {
isOpen: isSidebarOpen,
open: openSidebar,
close: closeSidebar,
} = useSidebar()
const route = useRoute()
watch(() => route.path, closeSidebar)
useCloseSidebarOnEscape(isSidebarOpen, closeSidebar)
provide('close-sidebar', closeSidebar)
provide('is-sidebar-open', isSidebarOpen)
// handle scrollBehavior with transition
const scrollPromise = useScrollPromise()
@ -11,7 +38,10 @@ const onBeforeLeave = scrollPromise.pending
<template>
<div class="theme-plume">
<Nav />
<Page></Page>
<Sidebar :open="isSidebarOpen" />
<LayoutContent>
<Page />
</LayoutContent>
</div>
</template>

View File

@ -0,0 +1,5 @@
@import '@vuepress/plugin-palette/palette';
$codeLang: 'c' 'cpp' 'cs' 'dart' 'docker' 'fs' 'go' 'java' 'kt' 'makefile' 'css'
'less' 'sass' 'scss' 'styl' 'html' 'js' 'json' 'ts' 'vue' 'jsx' 'md' 'php'
'py' 'rb' 'rs' 'sh' 'toml' 'yml' !default;

View File

@ -0,0 +1,277 @@
// ===============================
// Forked and modified from prismjs/themes/prism-tomorrow.css
code[class*='language-'],
pre[class*='language-'] {
color: var(--vp-code-block-color);
background: none;
font-family: var(--vp-font-family-mono);
font-size: var(--vp-code-font-size);
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: var(--vp-code-line-height);
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
/* Code blocks */
pre[class*='language-'] {
padding: 1em;
margin: 0.5em 0;
overflow: auto;
}
// :not(pre) > code[class*='language-'],
// pre[class*='language-'] {
// background: var(--vp-code-block-bg);
// }
// /* Inline code */
// :not(pre) > code[class*='language-'] {
// padding: 0.1em;
// border-radius: 0.3em;
// white-space: normal;
// }
.token.comment,
.token.block-comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #999;
}
.token.punctuation {
color: #ccc;
}
.token.tag,
.token.attr-name,
.token.namespace,
.token.deleted {
color: #ec5975;
}
.token.function-name {
color: #6196cc;
}
.token.boolean,
.token.number,
.token.function {
color: #f08d49;
}
.token.property,
.token.class-name,
.token.constant,
.token.symbol {
color: #f8c555;
}
.token.selector,
.token.important,
.token.atrule,
.token.keyword,
.token.builtin {
color: #cc99cd;
}
.token.string,
.token.char,
.token.attr-value,
.token.regex,
.token.variable {
color: #7ec699;
}
.token.operator,
.token.entity,
.token.url {
color: #67cdcc;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}
.token.inserted {
color: #3eaf7c;
}
// ===============================
.plume-content {
pre,
pre[class*='language-'] {
// line-height: 1.4;
padding: 1.3rem 1.5rem;
margin: 0 0 0.85rem 0;
border-radius: 6px;
overflow: auto;
scrollbar-width: thin;
scrollbar-color: var(--c-brand) var(--c-border);
&::-webkit-scrollbar {
width: 7px;
height: 7px;
}
&::-webkit-scrollbar-track {
background-color: var(--vp-code-block-bg);
}
&::-webkit-scrollbar-thumb {
background-color: rgba(220, 220, 220, 0.35);
border-radius: 3px;
}
code {
color: #fff;
padding: 0;
background-color: transparent;
border-radius: 0;
overflow-wrap: unset;
-webkit-font-smoothing: auto;
-moz-osx-font-smoothing: auto;
}
}
.line-number {
font-family: var(--vp-font-family-mono);
}
.code-tabs {
.div[class*='language-'] {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
}
.code-tabs-nav {
margin-bottom: 0rem;
}
}
div[class*='language-'] {
position: relative;
background-color: var(--vp-code-block-bg);
border-radius: 6px;
&::before {
content: attr(data-ext);
position: absolute;
z-index: 3;
top: 0.8em;
right: 1em;
font-size: 0.75rem;
color: var(--vp-code-line-number-color);
}
pre,
pre[class*='language-'] {
// force override the background color to be compatible with shiki
background: transparent !important;
position: relative;
z-index: 1;
}
.highlight-lines {
user-select: none;
padding-top: 1.3rem;
position: absolute;
top: 0;
left: 0;
width: 100%;
line-height: 1.4;
.highlight-line {
background-color: var(--vp-code-block-bg);
}
}
&:not(.line-numbers-mode) {
.line-numbers {
display: none;
}
}
&.line-numbers-mode {
.highlight-lines .highlight-line {
position: relative;
&::before {
content: ' ';
position: absolute;
z-index: 2;
left: 0;
top: 0;
display: block;
// width: var(--code-ln-wrapper-width);
height: 100%;
}
}
pre {
margin-left: 2rem;
padding-left: 1rem;
vertical-align: middle;
}
.line-numbers {
position: absolute;
top: 0;
left: 0;
width: 2rem;
height: 100%;
text-align: center;
z-index: 1;
color: var(--vp-code-line-number-color);
padding-top: 1.14rem;
line-height: var(--vp-code-line-height);
counter-reset: line-number;
border-right: var(--vp-code-block-divider-color) 1px solid;
.line-number {
position: relative;
z-index: 3;
user-select: none;
height: 1.5rem;
&::before {
counter-increment: line-number;
content: counter(line-number);
font-size: 0.85em;
}
}
}
}
}
// narrow mobile
@media (max-width: 419px) {
.plume-content {
div[class*='language-'] {
margin: 0.85rem -1.5rem;
border-radius: 0;
}
}
}

View File

@ -260,242 +260,242 @@
color: var(--vp-c-brand-dark);
}
.plume-content div[class*='language-'] {
position: relative;
margin: 16px -24px;
background-color: var(--vp-code-block-bg);
overflow-x: auto;
transition: background-color 0.5s;
}
// .plume-content div[class*='language-'] {
// position: relative;
// margin: 16px -24px;
// background-color: var(--vp-code-block-bg);
// overflow-x: auto;
// transition: background-color 0.5s;
// }
@media (min-width: 640px) {
.plume-content div[class*='language-'] {
border-radius: 8px;
margin: 16px 0;
}
}
// @media (min-width: 640px) {
// .plume-content div[class*='language-'] {
// border-radius: 8px;
// margin: 16px 0;
// }
// }
@media (max-width: 639px) {
.plume-content li div[class*='language-'] {
border-radius: 8px 0 0 8px;
}
}
// @media (max-width: 639px) {
// .plume-content li div[class*='language-'] {
// border-radius: 8px 0 0 8px;
// }
// }
.plume-content div[class*='language-'] + div[class*='language-'],
.plume-content div[class$='-api'] + div[class*='language-'],
.plume-content
div[class*='language-']
+ div[class$='-api']
> div[class*='language-'] {
margin-top: -8px;
}
// .plume-content div[class*='language-'] + div[class*='language-'],
// .plume-content div[class$='-api'] + div[class*='language-'],
// .plume-content
// div[class*='language-']
// + div[class$='-api']
// > div[class*='language-'] {
// margin-top: -8px;
// }
.plume-content [class*='language-'] pre,
.plume-content [class*='language-'] code {
/*rtl:ignore*/
direction: ltr;
/*rtl:ignore*/
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
// .plume-content [class*='language-'] pre,
// .plume-content [class*='language-'] code {
// /*rtl:ignore*/
// direction: ltr;
// /*rtl:ignore*/
// text-align: left;
// white-space: pre;
// word-spacing: normal;
// word-break: normal;
// word-wrap: normal;
// -moz-tab-size: 4;
// -o-tab-size: 4;
// tab-size: 4;
// -webkit-hyphens: none;
// -moz-hyphens: none;
// -ms-hyphens: none;
// hyphens: none;
// }
.plume-content [class*='language-'] pre {
position: relative;
z-index: 1;
margin: 0;
padding: 16px 0;
background: transparent;
overflow-x: auto;
}
// .plume-content [class*='language-'] pre {
// position: relative;
// z-index: 1;
// margin: 0;
// padding: 16px 0;
// background: transparent;
// overflow-x: auto;
// }
.plume-content [class*='language-'] code {
display: block;
padding: 0 24px;
width: fit-content;
min-width: 100%;
line-height: var(--vp-code-line-height);
font-size: var(--vp-code-font-size);
color: var(--vp-code-block-color);
transition: color 0.5s;
}
// .plume-content [class*='language-'] code {
// display: block;
// padding: 0 24px;
// width: fit-content;
// min-width: 100%;
// line-height: var(--vp-code-line-height);
// font-size: var(--vp-code-font-size);
// color: var(--vp-code-block-color);
// transition: color 0.5s;
// }
.plume-content [class*='language-'] code .highlighted {
background-color: var(--vp-code-line-highlight-color);
transition: background-color 0.5s;
margin: 0 -24px;
padding: 0 24px;
width: calc(100% + 2 * 24px);
display: inline-block;
}
// .plume-content [class*='language-'] code .highlighted {
// background-color: var(--vp-code-line-highlight-color);
// transition: background-color 0.5s;
// margin: 0 -24px;
// padding: 0 24px;
// width: calc(100% + 2 * 24px);
// display: inline-block;
// }
.plume-content [class*='language-'] code .highlighted.error {
background-color: var(--vp-code-line-error-color);
}
// .plume-content [class*='language-'] code .highlighted.error {
// background-color: var(--vp-code-line-error-color);
// }
.plume-content [class*='language-'] code .highlighted.warning {
background-color: var(--vp-code-line-warning-color);
}
// .plume-content [class*='language-'] code .highlighted.warning {
// background-color: var(--vp-code-line-warning-color);
// }
.plume-content [class*='language-'] code .diff {
transition: background-color 0.5s;
margin: 0 -24px;
padding: 0 24px;
width: calc(100% + 2 * 24px);
display: inline-block;
}
// .plume-content [class*='language-'] code .diff {
// transition: background-color 0.5s;
// margin: 0 -24px;
// padding: 0 24px;
// width: calc(100% + 2 * 24px);
// display: inline-block;
// }
.plume-content [class*='language-'] code .diff::before {
position: absolute;
left: 10px;
}
// .plume-content [class*='language-'] code .diff::before {
// position: absolute;
// left: 10px;
// }
.plume-content [class*='language-'] .has-focused-lines .line:not(.has-focus) {
filter: blur(0.095rem);
opacity: 0.4;
transition: filter 0.35s, opacity 0.35s;
}
// .plume-content [class*='language-'] .has-focused-lines .line:not(.has-focus) {
// filter: blur(0.095rem);
// opacity: 0.4;
// transition: filter 0.35s, opacity 0.35s;
// }
.plume-content [class*='language-'] .has-focused-lines .line:not(.has-focus) {
opacity: 0.7;
transition: filter 0.35s, opacity 0.35s;
}
// .plume-content [class*='language-'] .has-focused-lines .line:not(.has-focus) {
// opacity: 0.7;
// transition: filter 0.35s, opacity 0.35s;
// }
.plume-content
[class*='language-']:hover
.has-focused-lines
.line:not(.has-focus) {
filter: blur(0);
opacity: 1;
}
// .plume-content
// [class*='language-']:hover
// .has-focused-lines
// .line:not(.has-focus) {
// filter: blur(0);
// opacity: 1;
// }
.plume-content [class*='language-'] code .diff.remove {
background-color: var(--vp-code-line-diff-remove-color);
opacity: 0.7;
}
// .plume-content [class*='language-'] code .diff.remove {
// background-color: var(--vp-code-line-diff-remove-color);
// opacity: 0.7;
// }
.plume-content [class*='language-'] code .diff.remove::before {
content: '-';
color: var(--vp-code-line-diff-remove-symbol-color);
}
// .plume-content [class*='language-'] code .diff.remove::before {
// content: '-';
// color: var(--vp-code-line-diff-remove-symbol-color);
// }
.plume-content [class*='language-'] code .diff.add {
background-color: var(--vp-code-line-diff-add-color);
}
// .plume-content [class*='language-'] code .diff.add {
// background-color: var(--vp-code-line-diff-add-color);
// }
.plume-content [class*='language-'] code .diff.add::before {
content: '+';
color: var(--vp-code-line-diff-add-symbol-color);
}
// .plume-content [class*='language-'] code .diff.add::before {
// content: '+';
// color: var(--vp-code-line-diff-add-symbol-color);
// }
.plume-content div[class*='language-'].line-numbers-mode {
/*rtl:ignore*/
padding-left: 32px;
}
// .plume-content div[class*='language-'].line-numbers-mode {
// /*rtl:ignore*/
// padding-left: 32px;
// }
.plume-content .line-numbers-wrapper {
position: absolute;
top: 0;
bottom: 0;
/*rtl:ignore*/
left: 0;
z-index: 3;
/*rtl:ignore*/
border-right: 1px solid var(--vp-code-block-divider-color);
padding-top: 16px;
width: 32px;
text-align: center;
font-family: var(--vp-font-family-mono);
line-height: var(--vp-code-line-height);
font-size: var(--vp-code-font-size);
color: var(--vp-code-line-number-color);
transition: border-color 0.5s, color 0.5s;
}
// .plume-content .line-numbers {
// position: absolute;
// top: 0;
// bottom: 0;
// /*rtl:ignore*/
// left: 0;
// z-index: 3;
// /*rtl:ignore*/
// border-right: 1px solid var(--vp-code-block-divider-color);
// padding-top: 16px;
// width: 32px;
// text-align: center;
// font-family: var(--vp-font-family-mono);
// line-height: var(--vp-code-line-height);
// font-size: var(--vp-code-font-size);
// color: var(--vp-code-line-number-color);
// transition: border-color 0.5s, color 0.5s;
// }
.plume-content [class*='language-'] > button.copy {
/*rtl:ignore*/
direction: ltr;
position: absolute;
top: 8px;
/*rtl:ignore*/
right: 8px;
z-index: 3;
display: block;
justify-content: center;
align-items: center;
border-radius: 4px;
width: 40px;
height: 40px;
background-color: var(--vp-code-block-bg);
opacity: 0;
cursor: pointer;
background-image: var(--vp-icon-copy);
background-position: 50%;
background-size: 20px;
background-repeat: no-repeat;
transition: opacity 0.4s;
}
// .plume-content [class*='language-'] > button.copy {
// /*rtl:ignore*/
// direction: ltr;
// position: absolute;
// top: 8px;
// /*rtl:ignore*/
// right: 8px;
// z-index: 3;
// display: block;
// justify-content: center;
// align-items: center;
// border-radius: 4px;
// width: 40px;
// height: 40px;
// background-color: var(--vp-code-block-bg);
// opacity: 0;
// cursor: pointer;
// background-image: var(--vp-icon-copy);
// background-position: 50%;
// background-size: 20px;
// background-repeat: no-repeat;
// transition: opacity 0.4s;
// }
.plume-content [class*='language-']:hover > button.copy,
.plume-content [class*='language-'] > button.copy:focus {
opacity: 1;
}
// .plume-content [class*='language-']:hover > button.copy,
// .plume-content [class*='language-'] > button.copy:focus {
// opacity: 1;
// }
.plume-content [class*='language-'] > button.copy:hover {
background-color: var(--vp-code-copy-code-hover-bg);
}
// .plume-content [class*='language-'] > button.copy:hover {
// background-color: var(--vp-code-copy-code-hover-bg);
// }
.plume-content [class*='language-'] > button.copy.copied,
.plume-content [class*='language-'] > button.copy:hover.copied {
/*rtl:ignore*/
border-radius: 0 4px 4px 0;
background-color: var(--vp-code-copy-code-hover-bg);
background-image: var(--vp-icon-copied);
}
// .plume-content [class*='language-'] > button.copy.copied,
// .plume-content [class*='language-'] > button.copy:hover.copied {
// /*rtl:ignore*/
// border-radius: 0 4px 4px 0;
// background-color: var(--vp-code-copy-code-hover-bg);
// background-image: var(--vp-icon-copied);
// }
.plume-content [class*='language-'] > button.copy.copied::before,
.plume-content [class*='language-'] > button.copy:hover.copied::before {
position: relative;
/*rtl:ignore*/
left: -65px;
display: flex;
justify-content: center;
align-items: center;
/*rtl:ignore*/
border-radius: 4px 0 0 4px;
width: 64px;
height: 40px;
text-align: center;
font-size: 12px;
font-weight: 500;
color: var(--vp-code-copy-code-active-text);
background-color: var(--vp-code-copy-code-hover-bg);
white-space: nowrap;
content: 'Copied';
}
// .plume-content [class*='language-'] > button.copy.copied::before,
// .plume-content [class*='language-'] > button.copy:hover.copied::before {
// position: relative;
// /*rtl:ignore*/
// left: -65px;
// display: flex;
// justify-content: center;
// align-items: center;
// /*rtl:ignore*/
// border-radius: 4px 0 0 4px;
// width: 64px;
// height: 40px;
// text-align: center;
// font-size: 12px;
// font-weight: 500;
// color: var(--vp-code-copy-code-active-text);
// background-color: var(--vp-code-copy-code-hover-bg);
// white-space: nowrap;
// content: 'Copied';
// }
.plume-content [class*='language-'] > span.lang {
position: absolute;
top: 6px;
/*rtl:ignore*/
right: 12px;
z-index: 2;
font-size: 12px;
font-weight: 500;
color: var(--vp-c-text-dark-3);
transition: color 0.4s, opacity 0.4s;
}
// .plume-content [class*='language-'] > span.lang {
// position: absolute;
// top: 6px;
// /*rtl:ignore*/
// right: 12px;
// z-index: 2;
// font-size: 12px;
// font-weight: 500;
// color: var(--vp-c-text-dark-3);
// transition: color 0.4s, opacity 0.4s;
// }
.plume-content [class*='language-']:hover > button.copy + span.lang,
.plume-content [class*='language-'] > button.copy:focus + span.lang {
opacity: 0;
}
// .plume-content [class*='language-']:hover > button.copy + span.lang,
// .plume-content [class*='language-'] > button.copy:focus + span.lang {
// opacity: 0;
// }

View File

@ -4,3 +4,7 @@
@use 'nprogress';
@use 'utils';
@use 'content';
@use 'toc';
@use 'code';
@use '@vuepress/plugin-palette/style';

View File

@ -0,0 +1,64 @@
.theme-plume-toc {
border-left: solid 1px var(--vp-c-divider);
&::-webkit-scrollbar {
width: 0;
opacity: 0;
}
> .theme-plume-toc-list {
padding-left: 0;
}
}
.theme-plume-toc-list {
list-style: none;
font-size: 13px;
font-weight: 500;
.theme-plume-toc-link {
width: 100%;
display: inline-block;
color: var(--vp-c-text-2);
border-left: solid 3px transparent;
padding-left: 1rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 28px;
transition: color 0.25s, border-color 0.25s;
&.active {
color: var(--vp-c-text-1);
border-color: var((--c-text-accent));
}
}
.theme-plume-toc-list {
.theme-plume-toc-link {
border: none;
font-size: 14px;
color: var(--c-text-lighter);
&.active {
color: var(--c-text-accent);
}
}
}
}
.plume-theme-page-toc {
width: 12.5rem;
margin-left: 1.25rem;
.theme-plume-toc {
position: sticky;
top: calc(var(--navbar-height) + 1.25rem);
}
}
.archive-toc {
width: 4rem;
margin-left: 0;
position: sticky;
top: calc(var(--navbar-height) + 1.25rem);
}

View File

@ -399,3 +399,9 @@
--vp-carbon-ads-hover-text-color: var(--vp-c-brand);
--vp-carbon-ads-hover-poweredby-color: var(--vp-c-text-1);
}
:root {
--t-color: 0.25s;
--code-tabs-nav-bg-color: var(--vp-code-tab-b);
--code-bg-color: var(--vp-code-block-bg);
}

View File

@ -0,0 +1,14 @@
/**
* @method
* t: current time
* b: beginning value
* c: change in value
* d: duration
*/
export const tween = (t: number, b: number, c: number, d: number): number => {
return c * (t /= d) * t * t + b
}
export const linear = (t: number, b: number, c: number, d: number): number => {
return (c * t) / d + b
}

View File

@ -0,0 +1,65 @@
import { tween } from './animate.js'
export function getCssValue(el: HTMLElement | null, property: string): number {
const val = el?.ownerDocument?.defaultView?.getComputedStyle(el, null)?.[
property as any
]
const num = Number.parseInt(val as string, 10)
return Number.isNaN(num) ? 0 : num
}
export function getScrollTop(
target: Document | HTMLElement = document
): number {
if (target === document || !target) {
return (
window.pageYOffset ||
document.documentElement.scrollTop ||
document.body.scrollTop ||
0
)
} else {
return (target as HTMLElement).scrollTop
}
}
export function setScrollTop(
target: Document | HTMLElement = document,
scrollTop = 0
): void {
if (typeof target === 'number') {
scrollTop = target
target = document
document.documentElement.scrollTop = scrollTop
document.body.scrollTop = scrollTop
} else {
if (target === document) {
document.body.scrollTop = scrollTop || 0
document.documentElement.scrollTop = scrollTop || 0
} else {
;(target as HTMLElement).scrollTop = scrollTop || 0
}
}
}
export function scrollTo(
target: Document | HTMLElement,
top: number,
time = 300
): void {
if (target !== document) {
const currentTop = getScrollTop(target)
const step = Math.ceil(time / 16)
let currentStep = 0
const change = top - currentTop
const timer = setInterval(() => {
currentStep++
if (currentStep >= step) {
timer && clearInterval(timer)
}
setScrollTop(target, tween(currentStep, currentTop, change, step))
}, 1000 / 60)
} else {
window.scrollTo({ top, behavior: 'smooth' })
}
}

View File

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

View File

@ -41,3 +41,25 @@ export function normalize(path: string): string {
export function isExternal(path: string): boolean {
return EXTERNAL_URL_RE.test(path)
}
export function throttleAndDebounce(fn: () => void, delay: number): () => void {
// eslint-disable-next-line no-undef
let timeoutId: NodeJS.Timeout
let called = false
return () => {
if (timeoutId) {
clearTimeout(timeoutId)
}
if (!called) {
fn()
called = true
setTimeout(() => {
called = false
}, delay)
} else {
timeoutId = setTimeout(fn, delay)
}
}
}

View File

@ -14,6 +14,7 @@ import { nprogressPlugin } from '@vuepress/plugin-nprogress'
import { palettePlugin } from '@vuepress/plugin-palette'
import { prismjsPlugin } from '@vuepress/plugin-prismjs'
import { searchPlugin } from '@vuepress/plugin-search'
import { shikiPlugin } from '@vuepress/plugin-shiki'
import { themeDataPlugin } from '@vuepress/plugin-theme-data'
import { commentPlugin } from 'vuepress-plugin-comment2'
import { mdEnhancePlugin } from 'vuepress-plugin-md-enhance'
@ -49,7 +50,7 @@ export const setupPlugins = (
}),
localeOptions.notes ? notesDataPlugin(localeOptions.notes) : [],
activeHeaderLinksPlugin({
headerLinkSelector: 'a.theme-plume-toc-link',
headerLinkSelector: 'a.outline-link',
headerAnchorSelector: '.header-anchor',
delay: 200,
offset: 20,
@ -67,7 +68,7 @@ export const setupPlugins = (
options.mediumZoom !== false
? mediumZoomPlugin({
selector: '.page-content > img, .page-content :not(a) > img',
selector: '.plume-content > img, .plume-content :not(a) > img',
zoomOptions: {
background: 'var(--c-bg)',
},
@ -102,10 +103,15 @@ export const setupPlugins = (
options.docsearch !== false && !options.search
? docsearchPlugin(options.docsearch!)
: [],
options.prismjs !== false ? prismjsPlugin() : [],
options.prismjs !== false && !isProd ? prismjsPlugin() : [],
options.prismjs !== false && isProd
? shikiPlugin({
theme: 'material-palenight',
})
: [],
options.copyCode !== false
? copyCodePlugin({
selector: '.page-content div[class*="language-"] pre',
selector: '.plume-content div[class*="language-"] pre',
locales: {
'/': {
copy: '复制成功',

View File

@ -2,3 +2,4 @@ export * from './base.js'
export * from './frontmatter.js'
export * from './note.js'
export * from './options/index.js'
export * from './page.js'

View File

@ -0,0 +1,7 @@
export type PlumeThemePageData = {
git: {
createTime: number
updateTime: number
}
isBlogPost: boolean
}

27
pnpm-lock.yaml generated
View File

@ -292,6 +292,7 @@ importers:
'@vuepress/plugin-palette': 2.0.0-beta.60
'@vuepress/plugin-prismjs': 2.0.0-beta.60
'@vuepress/plugin-search': 2.0.0-beta.60
'@vuepress/plugin-shiki': 2.0.0-beta.60
'@vuepress/plugin-theme-data': 2.0.0-beta.60
'@vuepress/plugin-toc': 2.0.0-beta.60
'@vuepress/shared': 2.0.0-beta.60
@ -328,6 +329,7 @@ importers:
'@vuepress/plugin-palette': 2.0.0-beta.60
'@vuepress/plugin-prismjs': 2.0.0-beta.60
'@vuepress/plugin-search': 2.0.0-beta.60
'@vuepress/plugin-shiki': 2.0.0-beta.60
'@vuepress/plugin-theme-data': 2.0.0-beta.60
'@vuepress/plugin-toc': 2.0.0-beta.60
'@vuepress/shared': 2.0.0-beta.60
@ -3560,6 +3562,15 @@ packages:
- supports-color
dev: false
/@vuepress/plugin-shiki/2.0.0-beta.60:
resolution: {integrity: sha512-ENUZnuLPnXKqozA0sr2xrVuTWFBziYcXIi9sL6Dc1x3+AdBHxXA0oinPioJ/TJ5qgUF8rAFMZnUR/uwZQV+KQQ==}
dependencies:
'@vuepress/core': 2.0.0-beta.60
shiki: 0.12.1
transitivePeerDependencies:
- supports-color
dev: false
/@vuepress/plugin-theme-data/2.0.0-beta.60:
resolution: {integrity: sha512-3b34sXEAzShvUzeEMA/0JE4VrLxoMqGJOGMl0I9m0DKg2apgjRG6nYYq6gUnJW0gcUVK+tOOOHsMT6mTMs3xdA==}
dependencies:
@ -13876,6 +13887,14 @@ packages:
/shell-quote/1.7.4:
resolution: {integrity: sha512-8o/QEhSSRb1a5i7TFR0iM4G16Z0vYB2OQVs4G3aAFXjn3T6yEx8AZxy1PgDF7I00LZHYA3WxaSYIf5e5sAX8Rw==}
/shiki/0.12.1:
resolution: {integrity: sha512-aieaV1m349rZINEBkjxh2QbBvFFQOlgqYTNtCal82hHj4dDZ76oMlQIX+C7ryerBTDiga3e5NfH6smjdJ02BbQ==}
dependencies:
jsonc-parser: 3.2.0
vscode-oniguruma: 1.7.0
vscode-textmate: 8.0.0
dev: false
/side-channel/1.0.4:
resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==}
dependencies:
@ -15307,6 +15326,14 @@ packages:
fsevents: 2.3.2
dev: false
/vscode-oniguruma/1.7.0:
resolution: {integrity: sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==}
dev: false
/vscode-textmate/8.0.0:
resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==}
dev: false
/vue-demi/0.13.6_vue@3.2.47:
resolution: {integrity: sha512-02NYpxgyGE2kKGegRPYlNQSL1UWfA/+JqvzhGCOYjhfbLWXU5QQX0+9pAm/R2sCOPKr5NBxVIab7fvFU0B1RxQ==}
engines: {node: '>=12'}