feat(theme): add Nav components

This commit is contained in:
pengzhanbo 2023-02-10 04:39:12 +08:00
parent 5842436b6c
commit 1689b9c691
9 changed files with 390 additions and 6 deletions

View File

@ -0,0 +1,213 @@
<script lang="ts" setup>
import { useWindowScroll } from '@vueuse/core'
import { computed } from 'vue'
import { useSidebar } from '../../composables/sidebar.js'
import NavBarTitle from './NavBarTitle.vue'
defineProps<{
isScreenOpen: boolean
}>()
defineEmits<{
(e: 'toggle-screen'): void
}>()
const { y } = useWindowScroll()
const { hasSidebar } = useSidebar()
const classes = computed(() => ({
'has-sidebar': hasSidebar.value,
'fill': y.value > 0,
}))
</script>
<template>
<div class="navbar-wrapper" :class="classes">
<div class="container">
<div class="title">
<NavBarTitle />
</div>
<div class="content">
<div class="curtain"></div>
<div class="content-body">
<NavbarSearch class="search" />
</div>
</div>
</div>
</div>
</template>
<style scoped>
.navbar-wrapper {
position: relative;
border-bottom: 1px solid transparent;
padding: 0 8px 0 24px;
height: var(--vp-nav-height);
transition: border-color 0.5s, background-color 0.5s;
pointer-events: none;
}
.navbar-wrapper.has-sidebar {
border-bottom-color: var(--vp-c-gutter);
}
@media (min-width: 768px) {
.navbar-wrapper {
padding: 0 32px;
}
}
@media (min-width: 960px) {
.navbar-wrapper.has-sidebar {
border-bottom-color: transparent;
padding: 0;
}
.navbar-wrapper.fill:not(.has-sidebar) {
border-bottom-color: var(--vp-c-gutter);
background-color: var(--vp-nav-bg-color);
}
}
.container {
display: flex;
justify-content: space-between;
margin: 0 auto;
max-width: calc(var(--vp-layout-max-width) - 64px);
height: var(--vp-nav-height);
pointer-events: none;
}
.container :deep(*) {
pointer-events: auto;
}
@media (min-width: 960px) {
.navbar-wrapper.has-sidebar .container {
max-width: 100%;
}
}
.title {
flex-shrink: 0;
height: calc(var(--vp-nav-height) - 1px);
transition: background-color 0.5s;
}
@media (min-width: 960px) {
.navbar-wrapper.has-sidebar .title {
position: absolute;
top: 0;
left: 0;
z-index: 2;
padding: 0 32px;
width: var(--vp-sidebar-width);
height: var(--vp-nav-height);
background-color: transparent;
}
}
@media (min-width: 1440px) {
.navbar-wrapper.has-sidebar .title {
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
);
}
}
.content {
flex-grow: 1;
}
@media (min-width: 960px) {
.navbar-wrapper.has-sidebar .content {
position: relative;
z-index: 1;
padding-right: 32px;
padding-left: var(--vp-sidebar-width);
}
}
@media (min-width: 1440px) {
.navbar-wrapper.has-sidebar .content {
padding-right: calc((100vw - var(--vp-layout-max-width)) / 2 + 32px);
padding-left: calc(
(100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width)
);
}
}
.content-body {
display: flex;
justify-content: flex-end;
align-items: center;
height: calc(var(--vp-nav-height) - 1px);
transition: background-color 0.5s;
}
@media (min-width: 960px) {
.navbar-wrapper.has-sidebar .content-body,
.navbar-wrapper.fill .content-body {
position: relative;
background-color: var(--vp-nav-bg-color);
}
}
.menu + .translations::before,
.menu + .appearance::before,
.menu + .social-links::before,
.translations + .appearance::before,
.appearance + .social-links::before {
margin-right: 8px;
margin-left: 8px;
width: 1px;
height: 24px;
background-color: var(--vp-c-divider);
content: '';
}
.menu + .appearance::before,
.translations + .appearance::before {
margin-right: 16px;
}
.appearance + .social-links::before {
margin-left: 16px;
}
.social-links {
margin-right: -8px;
}
@media (min-width: 960px) {
.navbar-wrapper.has-sidebar .curtain {
position: absolute;
right: 0;
bottom: -31px;
width: calc(100% - var(--vp-sidebar-width));
height: 32px;
}
.navbar-wrapper.has-sidebar .curtain::before {
display: block;
width: 100%;
height: 32px;
background: linear-gradient(var(--vp-c-bg), transparent 70%);
content: '';
}
}
@media (min-width: 1440px) {
.navbar-wrapper.has-sidebar .curtain {
width: calc(
100% -
((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width))
);
}
}
</style>

View File

@ -0,0 +1,54 @@
<script lang="ts" setup>
import { useSiteLocaleData } from '@vuepress/client'
import { useThemeLocaleData } from '../../composables/themeData.js'
import VImage from '../VImage.vue'
const theme = useThemeLocaleData()
const site = useSiteLocaleData()
</script>
<template>
<div class="navbar-title">
<a class="title" :href="theme.home">
<VImage
v-if="theme.logo"
class="logo"
:image="{ light: theme.logo, dark: theme.logoDark || '' }"
/>
{{ site.title }}
</a>
</div>
</template>
<style scoped>
.title {
display: flex;
align-items: center;
border-bottom: 1px solid transparent;
width: 100%;
height: var(--vp-nav-height);
font-size: 16px;
font-weight: 600;
color: var(--vp-c-text-1);
transition: opacity 0.25s;
}
.title:hover {
opacity: 0.6;
}
@media (min-width: 960px) {
.title {
flex-shrink: 0;
}
.navbar-title.has-sidebar .title {
border-bottom-color: var(--vp-c-divider);
}
}
:deep(.logo) {
margin-right: 8px;
height: 24px;
}
</style>

View File

@ -0,0 +1 @@
<script setup lang="ts"></script>

View File

@ -0,0 +1,14 @@
<script lang="ts" setup>
import { provide } from 'vue'
import { useNav } from '../../composables/nav.js'
import Navbar from './Navbar.vue'
const { isScreenOpen, closeScreen, toggleScreen } = useNav()
provide('close-screen', closeScreen)
</script>
<template>
<div class="nav-wrapper">
<Navbar :is-screen-open="isScreenOpen"></Navbar>
</div>
</template>

View File

@ -1,4 +0,0 @@
<script lang="ts" setup></script>
<template>
<div class="navbar-wrapper"></div>
</template>

View File

@ -0,0 +1,50 @@
<script lang="ts" setup>
import { withBase } from '@vuepress/client'
defineProps<{
image:
| string
| { src: string; alt?: string }
| { dark: string; light: string; alt?: string }
alt?: string
}>()
</script>
<script lang="ts">
export default {
inheritAttrs: false,
}
</script>
<template>
<template v-if="image">
<img
v-if="typeof image === 'string' || 'src' in image"
class="plume-image"
v-bind="typeof image === 'string' ? $attrs : { ...image, ...$attrs }"
:src="withBase(typeof image === 'string' ? image : image.src)"
:alt="alt ?? (typeof image === 'string' ? '' : image.alt || '')"
/>
<template v-else>
<VImage
class="dark"
:image="image.dark"
:alt="image.alt"
v-bind="$attrs"
/>
<VImage
class="light"
:image="image.light"
:alt="image.alt"
v-bind="$attrs"
/>
</template>
</template>
</template>
<style scoped>
html:not(.dark) .plume-image.dark {
display: none;
}
.dark .plume-image.light {
display: none;
}
</style>

View File

@ -0,0 +1,45 @@
import type { Ref } from 'vue'
import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'
export interface UseNavReturn {
isScreenOpen: Ref<boolean>
openScreen: () => void
closeScreen: () => void
toggleScreen: () => void
}
export function useNav(): UseNavReturn {
const isScreenOpen = ref(false)
function openScreen(): void {
isScreenOpen.value = true
window.addEventListener('resize', closeScreenOnTabletWindow)
}
function closeScreen(): void {
isScreenOpen.value = false
window.removeEventListener('resize', closeScreenOnTabletWindow)
}
function toggleScreen(): void {
isScreenOpen.value ? closeScreen() : openScreen()
}
/**
* Close screen when the user resizes the window wider than tablet size.
*/
function closeScreenOnTabletWindow(): void {
window.outerWidth >= 768 && closeScreen()
}
const route = useRoute()
watch(() => route.path, closeScreen)
return {
isScreenOpen,
openScreen,
closeScreen,
toggleScreen,
}
}

View File

@ -0,0 +1,11 @@
import { computed } from 'vue'
export function useSidebar() {
const hasSidebar = computed(() => {
return false
})
return {
hasSidebar,
}
}

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import Navbar from '../components/Navbar/index.vue'
import Nav from '../components/Nav/index.vue'
import { useScrollPromise, useThemeLocaleData } from '../composables/index.js'
// handle scrollBehavior with transition
@ -9,6 +9,6 @@ const onBeforeLeave = scrollPromise.pending
</script>
<template>
<div class="theme-plume relative min-h-100vh">
<Navbar />
<Nav />
</div>
</template>