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
Show file tree
Hide file tree
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
Expand Up @@ -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()
Expand All @@ -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 />
Expand Down Expand Up @@ -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>

Expand Down Expand Up @@ -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'
Expand All @@ -20,6 +21,7 @@ defineEmits<{
}>()

const { y } = useWindowScroll()
const {isCollapsed, toggleCollapse: toggleSidebarCollapse } = useSidebarControl()
const { isHome, hasSidebar } = useLayout()

const classes = ref<Record<string, boolean>>({})
Expand All @@ -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">
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Up @@ -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;
Expand All @@ -63,10 +62,6 @@ const target = computed(() =>
.title {
flex-shrink: 0;
}

.VPNavBarTitle.has-sidebar .title {
border-bottom-color: var(--vp-c-divider);
}
}

:deep(.logo) {
Expand Down
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
Expand All @@ -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" />

Expand Down Expand Up @@ -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 {
Expand Down
9 changes: 8 additions & 1 deletion src/client/theme-default/composables/sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
}

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