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 VPSidebar from './components/VPSidebar.vue'
import VPSkipLink from './components/VPSkipLink.vue' import VPSkipLink from './components/VPSkipLink.vue'
import { useData } from './composables/data' import { useData } from './composables/data'
import { registerWatchers } from './composables/layout' import { registerWatchers, useLayout } from './composables/layout'
import { useSidebarControl } from './composables/sidebar' import { useSidebarControl } from './composables/sidebar'
const { const {
isOpen: isSidebarOpen, isOpen: isSidebarOpen,
open: openSidebar, open: openSidebar,
close: closeSidebar close: closeSidebar,
isCollapsed
} = useSidebarControl() } = useSidebarControl()
const { hasSidebar } = useLayout()
registerWatchers({ closeSidebar }) registerWatchers({ closeSidebar })
const { frontmatter } = useData() const { frontmatter } = useData()
@ -25,13 +28,21 @@ const slots = useSlots()
const heroImageSlotExists = computed(() => !!slots['home-hero-image']) const heroImageSlotExists = computed(() => !!slots['home-hero-image'])
provide('hero-image-slot-exists', heroImageSlotExists) 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> </script>
<template> <template>
<div <div
v-if="frontmatter.layout !== false" v-if="frontmatter.layout !== false"
class="Layout" class="Layout"
:class="frontmatter.pageClass" :class="layoutClasses"
> >
<slot name="layout-top" /> <slot name="layout-top" />
<VPSkipLink /> <VPSkipLink />
@ -92,4 +103,55 @@ provide('hero-image-slot-exists', heroImageSlotExists)
flex-direction: column; flex-direction: column;
min-height: 100vh; 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> </style>

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

@ -4,8 +4,10 @@ import { inBrowser } from 'vitepress'
import { ref, watch } from 'vue' import { ref, watch } from 'vue'
import { useLayout } from '../composables/layout' import { useLayout } from '../composables/layout'
import VPSidebarGroup from './VPSidebarGroup.vue' import VPSidebarGroup from './VPSidebarGroup.vue'
import { useSidebarControl } from '../composables/sidebar'
const { sidebarGroups, hasSidebar } = useLayout() const { sidebarGroups, hasSidebar } = useLayout()
const { isCollapsed } = useSidebarControl()
const props = defineProps<{ const props = defineProps<{
open: boolean open: boolean
@ -41,7 +43,7 @@ watch(
<aside <aside
v-if="hasSidebar" v-if="hasSidebar"
class="VPSidebar" class="VPSidebar"
:class="{ open }" :class="{ open, collapsed: isCollapsed }"
ref="navEl" ref="navEl"
@click.stop @click.stop
> >
@ -88,8 +90,11 @@ watch(
opacity: 1; opacity: 1;
visibility: visible; visibility: visible;
transform: translateX(0); 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 { .dark .VPSidebar {
@ -106,6 +111,22 @@ watch(
visibility: visible; visibility: visible;
box-shadow: none; box-shadow: none;
transform: translateX(0); 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)); 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); 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) { @media (min-width: 960px) {

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

@ -85,6 +85,9 @@
.vpi-corner-down-left { .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"); --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 { :root {
/* clipboard */ /* 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"); --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