feat(theme): add Nav components
This commit is contained in:
parent
5842436b6c
commit
1689b9c691
213
packages/theme/src/client/components/Nav/NavBar.vue
Normal file
213
packages/theme/src/client/components/Nav/NavBar.vue
Normal 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>
|
||||
54
packages/theme/src/client/components/Nav/NavBarTitle.vue
Normal file
54
packages/theme/src/client/components/Nav/NavBarTitle.vue
Normal 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>
|
||||
1
packages/theme/src/client/components/Nav/NavScreen.vue
Normal file
1
packages/theme/src/client/components/Nav/NavScreen.vue
Normal file
@ -0,0 +1 @@
|
||||
<script setup lang="ts"></script>
|
||||
14
packages/theme/src/client/components/Nav/index.vue
Normal file
14
packages/theme/src/client/components/Nav/index.vue
Normal 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>
|
||||
@ -1,4 +0,0 @@
|
||||
<script lang="ts" setup></script>
|
||||
<template>
|
||||
<div class="navbar-wrapper"></div>
|
||||
</template>
|
||||
50
packages/theme/src/client/components/VImage.vue
Normal file
50
packages/theme/src/client/components/VImage.vue
Normal 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>
|
||||
45
packages/theme/src/client/composables/nav.ts
Normal file
45
packages/theme/src/client/composables/nav.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
11
packages/theme/src/client/composables/sidebar.ts
Normal file
11
packages/theme/src/client/composables/sidebar.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { computed } from 'vue'
|
||||
|
||||
export function useSidebar() {
|
||||
const hasSidebar = computed(() => {
|
||||
return false
|
||||
})
|
||||
|
||||
return {
|
||||
hasSidebar,
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user