feat(theme): add page components
This commit is contained in:
parent
4e98011b65
commit
764c58693e
@ -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",
|
||||
|
||||
@ -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>
|
||||
48
packages/theme/src/client/components/LayoutContent.vue
Normal file
48
packages/theme/src/client/components/LayoutContent.vue
Normal 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>
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
94
packages/theme/src/client/components/PageAside.vue
Normal file
94
packages/theme/src/client/components/PageAside.vue
Normal 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>
|
||||
51
packages/theme/src/client/components/PageAsideItem.vue
Normal file
51
packages/theme/src/client/components/PageAsideItem.vue
Normal 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>
|
||||
148
packages/theme/src/client/components/Sidebar.vue
Normal file
148
packages/theme/src/client/components/Sidebar.vue
Normal 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>
|
||||
223
packages/theme/src/client/components/SidebarItem.vue
Normal file
223
packages/theme/src/client/components/SidebarItem.vue
Normal 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>
|
||||
148
packages/theme/src/client/components/Toc.ts
Normal file
148
packages/theme/src/client/components/Toc.ts
Normal 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
|
||||
140
packages/theme/src/client/composables/aside.ts
Normal file
140
packages/theme/src/client/composables/aside.ts
Normal 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]
|
||||
}
|
||||
@ -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'
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
5
packages/theme/src/client/styles/_variables.scss
Normal file
5
packages/theme/src/client/styles/_variables.scss
Normal 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;
|
||||
277
packages/theme/src/client/styles/code.scss
Normal file
277
packages/theme/src/client/styles/code.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
// }
|
||||
|
||||
@ -4,3 +4,7 @@
|
||||
@use 'nprogress';
|
||||
@use 'utils';
|
||||
@use 'content';
|
||||
@use 'toc';
|
||||
@use 'code';
|
||||
|
||||
@use '@vuepress/plugin-palette/style';
|
||||
|
||||
64
packages/theme/src/client/styles/toc.scss
Normal file
64
packages/theme/src/client/styles/toc.scss
Normal 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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
14
packages/theme/src/client/utils/animate.ts
Normal file
14
packages/theme/src/client/utils/animate.ts
Normal 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
|
||||
}
|
||||
65
packages/theme/src/client/utils/dom.ts
Normal file
65
packages/theme/src/client/utils/dom.ts
Normal 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' })
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
export * from './shared.js'
|
||||
export * from './normalizeLink.js'
|
||||
export * from './socialIcons.js'
|
||||
export * from './dom.js'
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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: '复制成功',
|
||||
|
||||
@ -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'
|
||||
|
||||
7
packages/theme/src/shared/page.ts
Normal file
7
packages/theme/src/shared/page.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export type PlumeThemePageData = {
|
||||
git: {
|
||||
createTime: number
|
||||
updateTime: number
|
||||
}
|
||||
isBlogPost: boolean
|
||||
}
|
||||
27
pnpm-lock.yaml
generated
27
pnpm-lock.yaml
generated
@ -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'}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user