feat(default-theme): collapsible sidbar

Signed-off-by: Artea <sepush@outlook.com>
pull/4739/head
Artea 5 months ago
parent 26cb685adf
commit 6a33280175

@ -8,15 +8,18 @@ import VPNav from './components/VPNav.vue'
import VPSidebar from './components/VPSidebar.vue'
import VPSkipLink from './components/VPSkipLink.vue'
import { useData } from './composables/data'
import { registerWatchers } from './composables/layout'
import { registerWatchers, useLayout } from './composables/layout'
import { useSidebarControl } from './composables/sidebar'
const {
isOpen: isSidebarOpen,
open: openSidebar,
close: closeSidebar
close: closeSidebar,
isCollapsed
} = useSidebarControl()
const { hasSidebar } = useLayout()
registerWatchers({ closeSidebar })
const { frontmatter } = useData()
@ -25,13 +28,21 @@ const slots = useSlots()
const heroImageSlotExists = computed(() => !!slots['home-hero-image'])
provide('hero-image-slot-exists', heroImageSlotExists)
const layoutClasses = computed(() => {
return {
[String(frontmatter.value.pageClass || '')]: !!frontmatter.value.pageClass,
'has-sidebar': hasSidebar.value,
'sidebar-collapsed': isCollapsed.value && hasSidebar.value
}
})
</script>
<template>
<div
v-if="frontmatter.layout !== false"
class="Layout"
:class="frontmatter.pageClass"
:class="layoutClasses"
>
<slot name="layout-top" />
<VPSkipLink />
@ -92,4 +103,55 @@ provide('hero-image-slot-exists', heroImageSlotExists)
flex-direction: column;
min-height: 100vh;
}
@media (min-width: 960px) {
.Layout.has-sidebar.sidebar-collapsed {
--vp-sidebar-width: 0px;
}
.Layout.has-sidebar.sidebar-collapsed .VPContent {
padding-left: 24px;
}
.Layout.has-sidebar.sidebar-collapsed .VPContent :deep(.VPDoc .content-container),
.Layout.has-sidebar.sidebar-collapsed .VPContent :deep(.VPDoc .content) {
max-width: 752px ;
}
.Layout.has-sidebar.sidebar-collapsed .VPContent :deep(.VPDoc .container) {
max-width: 992px;
}
.VPContent,
.VPContent :deep(.VPDoc .container),
.VPContent :deep(.VPDoc .content-container),
.VPContent :deep(.VPDoc .content),
.VPLocalNav,
.VPNavBar.has-sidebar .title,
.VPNavBar.has-sidebar .content,
.VPNavBar.has-sidebar .divider {
transition: padding-left 0.25s ease, margin-left 0.25s ease, width 0.25s ease, max-width 0.25s ease, transform 0.25s ease, opacity 0.25s ease;
}
.Layout.has-sidebar.sidebar-collapsed .VPLocalNav.has-sidebar {
padding-left: 24px;
}
.Layout.has-sidebar.sidebar-collapsed .VPNavBar.has-sidebar .content {
padding-left: var(--vp-nav-padding-x, 32px);
}
}
@media (min-width: 1440px) {
.Layout.has-sidebar.sidebar-collapsed .VPContent :deep(.VPDoc .content-container),
.Layout.has-sidebar.sidebar-collapsed .VPContent :deep(.VPDoc .content) {
max-width: 100%;
}
.Layout.has-sidebar.sidebar-collapsed .VPContent :deep(.VPDoc .container) {
max-width: 100%;
justify-content: space-between;
}
}
</style>

@ -1,7 +1,8 @@
<script lang="ts" setup>
import { useWindowScroll } from '@vueuse/core'
import { ref, watchPostEffect } from 'vue'
import { ref, computed, watchPostEffect } from 'vue'
import { useLayout } from '../composables/layout'
import { useSidebarControl } from '../composables/sidebar'
import VPNavBarAppearance from './VPNavBarAppearance.vue'
import VPNavBarExtra from './VPNavBarExtra.vue'
import VPNavBarHamburger from './VPNavBarHamburger.vue'
@ -20,34 +21,56 @@ defineEmits<{
}>()
const { y } = useWindowScroll()
const { isCollapsed, toggleCollapse: toggleSidebarCollapse } = useSidebarControl()
const { isHome, hasSidebar } = useLayout()
const classes = ref<Record<string, boolean>>({})
const isSidebarExpanded = computed(() => {
return hasSidebar.value && !isCollapsed.value;
});
watchPostEffect(() => {
classes.value = {
'has-sidebar': hasSidebar.value,
'home': isHome.value,
'top': y.value === 0,
'screen-open': props.isScreenOpen
'screen-open': props.isScreenOpen,
'sidebar-effectively-collapsed': isSidebarExpanded.value,
}
})
</script>
<template>
<div class="VPNavBar" :class="classes">
<div class="wrapper">
<div class="container">
<div class="title">
<VPNavBarTitle>
<div class="title" v-if="isSidebarExpanded">
<VPNavBarTitle >
<template #nav-bar-title-before><slot name="nav-bar-title-before" /></template>
<template #nav-bar-title-after><slot name="nav-bar-title-after" /></template>
</VPNavBarTitle>
<button
class="sidebar-toggle-button title-area-toggle"
@click="toggleSidebarCollapse"
aria-label="collapse sidebar"
>
<span class="vpi-collapse icon" />
</button>
</div>
<div class="content">
<div class="content-body">
<slot name="nav-bar-content-before" />
<button
v-if="!isSidebarExpanded&&!isHome"
class="sidebar-toggle-button"
@click="toggleSidebarCollapse"
aria-label="expand sidebar"
>
<span class="vpi-collapse icon is-collapsed" />
</button>
<VPNavBarSearch class="search" />
<VPNavBarMenu class="menu" />
<VPNavBarTranslations class="translations" />
@ -140,6 +163,9 @@ watchPostEffect(() => {
flex-shrink: 0;
height: calc(var(--vp-nav-height) - 1px);
transition: background-color 0.5s;
display: flex;
align-items: center;
justify-content: space-between;
}
@media (min-width: 960px) {
@ -269,4 +295,56 @@ watchPostEffect(() => {
background-color: var(--vp-c-gutter);
}
}
.sidebar-toggle-button {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
color: var(--vp-c-text-2);
background-color: transparent;
border: none;
border-radius: 6px;
transition: background-color 0.25s, color 0.25s;
pointer-events: auto;
}
.sidebar-toggle-button:hover {
background-color: var(--vp-c-bg-mute);
color: var(--vp-c-text-1);
}
.sidebar-toggle-button .icon {
width: 20px;
height: 20px;
transition: transform 0.25s ease;
}
.sidebar-toggle-button .icon.is-collapsed {
transform: rotate(180deg);
}
.title-area-toggle {
display: none;
}
@media (min-width: 960px) {
.VPNavBar.has-sidebar .title-area-toggle {
display: flex;
}
.VPNavBar.has-sidebar.sidebar-effectively-collapsed .search-area-toggle {
display: flex;
}
}
.title :deep(.VPNavBarTitle) {
flex-shrink: 0;
}
.content-body {
display: flex;
align-items:center;
}
</style>

@ -4,8 +4,10 @@ import { inBrowser } from 'vitepress'
import { ref, watch } from 'vue'
import { useLayout } from '../composables/layout'
import VPSidebarGroup from './VPSidebarGroup.vue'
import { useSidebarControl } from '../composables/sidebar'
const { sidebarGroups, hasSidebar } = useLayout()
const { isCollapsed } = useSidebarControl()
const props = defineProps<{
open: boolean
@ -41,7 +43,7 @@ watch(
<aside
v-if="hasSidebar"
class="VPSidebar"
:class="{ open }"
:class="{ open, collapsed: isCollapsed }"
ref="navEl"
@click.stop
>
@ -88,8 +90,11 @@ watch(
opacity: 1;
visibility: visible;
transform: translateX(0);
transition: opacity 0.25s,
transform 0.5s cubic-bezier(0.19, 1, 0.22, 1);
}
.VPSidebar.collapsed .nav,
.VPSidebar.collapsed .curtain {
display: none;
}
.dark .VPSidebar {
@ -106,6 +111,22 @@ watch(
visibility: visible;
box-shadow: none;
transform: translateX(0);
transition: transform 0.25s ease, width 0.25s ease, padding-left 0.25s ease, padding-right 0.25s ease;
}
.VPSidebar.collapsed {
transform: translateX(0);
padding-left: 0;
padding-right: 0;
border-left: none;
border-right: none;
overflow: hidden;
display: none;
}
.VPSidebar.collapsed .nav,
.VPSidebar.collapsed .curtain {
display: none;
}
}
@ -114,6 +135,12 @@ watch(
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);
}
.VPSidebar.collapsed {
transform: translateX(0);
padding-left: 0;
padding-right: 0;
}
}
@media (min-width: 960px) {

@ -14,6 +14,7 @@ import { hasActiveLink as containsActiveLink } from '../support/sidebar'
import { useData } from './data'
const isOpen = ref(false)
const isCollapsed = ref(false)
/**
* a11y: cache the element that opened the Sidebar (the menu button) then
@ -57,11 +58,17 @@ export function useSidebarControl() {
isOpen.value ? close() : open()
}
function toggleCollapse() {
isCollapsed.value = !isCollapsed.value
}
return {
isOpen,
open,
close,
toggle
toggle,
isCollapsed,
toggleCollapse
}
}

@ -85,6 +85,9 @@
.vpi-corner-down-left {
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='m9 10-5 5 5 5'/%3E%3Cpath d='M20 4v7a4 4 0 0 1-4 4H4'/%3E%3C/svg%3E");
}
.vpi-collapse {
--icon: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUiPgogICAgICAgIDxyZWN0IHdpZHRoPSIxOCIgaGVpZ2h0PSIxOCIgeD0iMyIgeT0iMyIgcng9IjIiLz4KICAgICAgICA8cGF0aCBkPSJNOSAzdjE4Ii8+CiAgICA8L3N2Zz4=');
}
:root {
/* clipboard */
--vp-icon-copy: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='rgba(128,128,128,1)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='8' height='4' x='8' y='2' rx='1' ry='1'/%3E%3Cpath d='M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'/%3E%3C/svg%3E");

Loading…
Cancel
Save