Skip to content

feat(default-theme): collapsible sidebar #4739

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions src/client/theme-default/Layout.vue
Original file line number Diff line number Diff line change
@@ -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,16 @@ provide('hero-image-slot-exists', heroImageSlotExists)
flex-direction: column;
min-height: 100vh;
}
@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>
10 changes: 8 additions & 2 deletions src/client/theme-default/components/VPContent.vue
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
<script setup lang="ts">
import NotFound from '../NotFound.vue'
import { useData } from '../composables/data'
import { useLayout } from '../composables/layout'
import { useLayout, } from '../composables/layout'
import {useSidebarControl} from '../composables/sidebar'
import VPDoc from './VPDoc.vue'
import VPHome from './VPHome.vue'
import VPPage from './VPPage.vue'
const { page, frontmatter } = useData()
const { isHome, hasSidebar } = useLayout()
const {isCollapsed} = useSidebarControl()
</script>

<template>
<div
class="VPContent"
id="VPContent"
:class="{ 'has-sidebar': hasSidebar, 'is-home': isHome }"
:class="{ 'has-sidebar': hasSidebar, 'is-home': isHome ,'collapsed':isCollapsed
}"
>
<slot name="not-found" v-if="page.isNotFound"><NotFound /></slot>

@@ -91,5 +94,8 @@ const { isHome, hasSidebar } = useLayout()
padding-right: calc((100vw - var(--vp-layout-max-width)) / 2);
padding-left: calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width));
}
.VPContent.has-sidebar.collapsed {
padding-left: calc(100vw - var(--vp-layout-max-width)) / 2 ;
}
}
</style>
68 changes: 62 additions & 6 deletions src/client/theme-default/components/VPNavBar.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<script lang="ts" setup>
import { useWindowScroll } from '@vueuse/core'
import { useMediaQuery, useWindowScroll } from '@vueuse/core'
import { ref, 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,6 +21,7 @@ defineEmits<{
}>()
const { y } = useWindowScroll()
const {isCollapsed, toggleCollapse: toggleSidebarCollapse } = useSidebarControl()
const { isHome, hasSidebar } = useLayout()
const classes = ref<Record<string, boolean>>({})
@@ -29,20 +31,31 @@ watchPostEffect(() => {
'has-sidebar': hasSidebar.value,
'home': isHome.value,
'top': y.value === 0,
'collapsed':isCollapsed.value,
'screen-open': props.isScreenOpen
}
})
const is1440 = useMediaQuery('(min-width: 1440px)')
</script>

<template>
<div class="VPNavBar" :class="classes">
<div class="wrapper">
<div class="container">
<div class="title">
<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>
<div class="title">
<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
v-if="!isHome&&is1440"
class="sidebar-toggle-button"
@click="toggleSidebarCollapse"
aria-label="collapse sidebar"
>
<span class="vpi-collapse icon" />
</button>
</div>

<div class="content">
@@ -94,6 +107,9 @@ watchPostEffect(() => {
.VPNavBar:not(.has-sidebar):not(.home.top) {
background-color: var(--vp-nav-bg-color);
}
.VPNavBar.collapsed {
background-color: var(--vp-nav-bg-color);
}
}
.wrapper {
@@ -140,6 +156,17 @@ 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;
}
.VPNavBar:not(.home):not(.collapsed).has-sidebar .title{
border-bottom: 1px solid var(--vp-c-divider);
width: calc((100% - (var(--vp-layout-max-width) - 64px)) / 2 + var(--vp-sidebar-width) - 32px - 20px)
}
.VPNavBar:not(.home).collapsed .title{
background-color: var(--vp-nav-bg-color);
border-bottom: 1px solid var(--vp-c-divider);
}
@media (min-width: 960px) {
@@ -269,4 +296,33 @@ 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);
}
</style>
5 changes: 0 additions & 5 deletions src/client/theme-default/components/VPNavBarTitle.vue
Original file line number Diff line number Diff line change
@@ -50,7 +50,6 @@ const target = computed(() =>
.title {
display: flex;
align-items: center;
border-bottom: 1px solid transparent;
width: 100%;
height: var(--vp-nav-height);
font-size: 16px;
@@ -63,10 +62,6 @@ const target = computed(() =>
.title {
flex-shrink: 0;
}
.VPNavBarTitle.has-sidebar .title {
border-bottom-color: var(--vp-c-divider);
}
}
:deep(.logo) {
41 changes: 34 additions & 7 deletions src/client/theme-default/components/VPSidebar.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
<script lang="ts" setup>
import { useScrollLock } from '@vueuse/core'
import { useEventListener, useScrollLock } from '@vueuse/core'
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 { isHome,sidebarGroups } = useLayout()
const { isCollapsed } = useSidebarControl()
const props = defineProps<{
open: boolean
@@ -28,22 +30,33 @@ watch(
const key = ref(0)
watch(
sidebarGroups,
() => {
key.value += 1
},
{ deep: true }
)
useEventListener(document,'mousemove',(e)=>{
if(isHome.value) return
if(e.clientX<5){
navEl.value?.classList.add('expanded')
}else if(e.target instanceof Node &&!navEl.value?.contains(e.target)&&e.pageX>272){
navEl.value?.classList.remove('expanded')
}
})
</script>

<template>
<aside
v-if="hasSidebar"
<aside
class="VPSidebar"
:class="{ open }"
:class="{ open ,collapsed:isCollapsed}"
ref="navEl"
@click.stop
v-if="!isHome"
>
<div class="curtain" />

@@ -84,12 +97,26 @@ watch(
overscroll-behavior: contain;
}
.collapsed{
transform: translateX(-100%);
opacity: 0 !important;
transition: opacity 0.25s, transform 0.25s ease;
overscroll-behavior: contain;
}
.expanded{
transform: translateX(0);
opacity: 1 !important;
transition: opacity 0.25s, transform 0.25s ease;
overscroll-behavior: contain;
width: calc(100vw - 64px);
max-width: 320px;
z-index: var(--vp-z-index-sidebar) !important;
}
.VPSidebar.open {
opacity: 1;
visibility: visible;
transform: translateX(0);
transition: opacity 0.25s,
transform 0.5s cubic-bezier(0.19, 1, 0.22, 1);
}
.dark .VPSidebar {
9 changes: 8 additions & 1 deletion src/client/theme-default/composables/sidebar.ts
Original file line number Diff line number Diff line change
@@ -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
}
}

3 changes: 3 additions & 0 deletions src/client/theme-default/styles/icons.css
Original file line number Diff line number Diff line change
@@ -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");