Files
fox/src/.vitepress/theme/components/CustomSidebar.vue
T
2026-05-13 08:53:26 +03:00

138 lines
3.4 KiB
Vue

<script lang="ts" setup>
import { useScrollLock } from '@vueuse/core'
import { inBrowser } from 'vitepress'
import { ref, watch } from 'vue'
import { useSidebar } from 'vitepress/dist/client/theme-default/composables/sidebar'
import VPSidebarItem from 'vitepress/dist/client/theme-default/components/VPSidebarItem.vue'
const { sidebarGroups, hasSidebar } = useSidebar()
const props = defineProps<{
open: boolean
}>()
// a11y: focus Nav element when menu has opened
const navEl = ref<HTMLElement | null>(null)
const isLocked = useScrollLock(inBrowser ? document.body : null)
watch(
[props, navEl],
() => {
if (props.open) {
isLocked.value = true
navEl.value?.focus()
} else isLocked.value = false
},
{ immediate: true, flush: 'post' }
)
</script>
<template>
<aside
v-if="hasSidebar"
class="VPSidebar CustomSidebar"
:class="{ open }"
ref="navEl"
@click.stop
>
<div class="curtain" />
<nav class="nav" id="VPSidebarNav" aria-labelledby="sidebar-aria-label" tabindex="-1">
<span class="visually-hidden" id="sidebar-aria-label">
Sidebar Navigation
</span>
<slot name="sidebar-nav-before" />
<div v-for="group in sidebarGroups" :key="group.text ?? ''" class="group">
<!-- Named group with collapsible items render via VPSidebarItem as-is -->
<template v-if="group.items && group.text">
<VPSidebarItem :item="group" :depth="0" />
</template>
<!-- Anonymous group (flat sidebar) render items one by one to handle WIP -->
<template v-else-if="group.items">
<template v-for="item in group.items" :key="item.text">
<div v-if="(item as any).wip" class="wip-item" data-tooltip="Раздел в разработке">
<span class="text">{{ item.text }}</span>
</div>
<VPSidebarItem v-else :item="item" :depth="0" />
</template>
</template>
<VPSidebarItem v-else :item="group" :depth="0" />
</div>
<slot name="sidebar-nav-after" />
</nav>
</aside>
</template>
<style lang="scss" scoped>
.VPSidebar {
position: fixed;
top: var(--vp-layout-top-height, 0px);
bottom: 0;
left: 0;
z-index: var(--vp-z-index-sidebar);
padding: 0px 0px 96px 0px;
width: 320px;
background-color: var(--vp-sidebar-bg-color);
opacity: 0;
box-shadow: var(--vp-c-shadow-3);
overflow-x: hidden;
overflow-y: auto;
transform: translateX(-100%);
transition: opacity 0.5s, transform 0.25s ease;
overscroll-behavior: contain;
}
.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 {
box-shadow: var(--vp-shadow-1);
}
@media (min-width: 960px) {
.VPSidebar {
padding-top: var(--vp-nav-height);
background-color: var(--vp-sidebar-bg-color);
opacity: 1;
visibility: visible;
box-shadow: none;
transform: translateX(0);
}
}
@media (min-width: 960px) {
.curtain {
position: sticky;
top: -64px;
left: 0;
z-index: 1;
margin-top: calc(var(--vp-nav-height) * -1);
margin-right: -32px;
margin-left: -32px;
height: var(--vp-nav-height);
background-color: var(--vp-sidebar-bg-color);
}
}
.nav {
outline: 0;
padding-top: 24px;
width: 100%;
}
@media (min-width: 960px) {
.group {
width: 100%;
}
}
</style>