Files
Starlight_Lancher/apps/app-frontend/src/components/ui/NavRail.vue

117 lines
2.7 KiB
Vue

<template>
<div ref="rail" class="nav-rail relative flex flex-col gap-[0.5rem]">
<slot />
<div
class="nav-rail-slider pointer-events-none absolute rounded-full"
:class="[
subpageSelected ? 'bg-button-bg' : 'bg-button-bgSelected',
transitionsEnabled ? 'nav-rail-slider-transition' : '',
]"
:style="sliderStyle"
aria-hidden="true"
/>
</div>
</template>
<script setup lang="ts">
import { computed, nextTick, onMounted, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const rail = ref<HTMLElement | null>(null)
const hasActive = ref(false)
const subpageSelected = ref(false)
const sliderReady = ref(false)
const transitionsEnabled = ref(false)
const top = ref(0)
const bottom = ref(0)
const left = ref(0)
const width = ref(0)
const topDelay = ref('0ms')
const bottomDelay = ref('0ms')
const STAGGER_DELAY = '120ms'
const sliderStyle = computed(() => ({
top: `${top.value}px`,
bottom: `${bottom.value}px`,
left: `${left.value}px`,
width: `${width.value}px`,
opacity: sliderReady.value && hasActive.value ? 1 : 0,
}))
function positionSlider() {
const container = rail.value
if (!container) return
const el = container.querySelector<HTMLElement>('.router-link-active, .subpage-active')
if (!el?.offsetParent || container.offsetHeight === 0) {
hasActive.value = false
return
}
subpageSelected.value = el.classList.contains('subpage-active')
const newTop = el.offsetTop
const newBottom = container.offsetHeight - el.offsetTop - el.offsetHeight
const movingDown = newTop > top.value
topDelay.value = movingDown ? STAGGER_DELAY : '0ms'
bottomDelay.value = movingDown ? '0ms' : STAGGER_DELAY
top.value = newTop
bottom.value = newBottom
left.value = el.offsetLeft
width.value = el.offsetWidth
hasActive.value = true
if (!sliderReady.value) {
sliderReady.value = true
requestAnimationFrame(() => {
transitionsEnabled.value = true
})
}
}
async function updateSlider() {
await nextTick()
positionSlider()
}
onMounted(updateSlider)
watch(() => [route.path, route.query], updateSlider)
</script>
<style scoped>
.nav-rail :deep(a),
.nav-rail :deep(button) {
position: relative;
z-index: 1;
}
.nav-rail :deep(a.router-link-active),
.nav-rail :deep(a.subpage-active),
.nav-rail :deep(button.router-link-active),
.nav-rail :deep(button.subpage-active) {
background-color: transparent;
}
.nav-rail-slider {
z-index: 0;
}
.nav-rail-slider-transition {
transition:
top 150ms cubic-bezier(0.4, 0, 0.2, 1) v-bind(topDelay),
bottom 150ms cubic-bezier(0.4, 0, 0.2, 1) v-bind(bottomDelay),
left 150ms cubic-bezier(0.4, 0, 0.2, 1),
width 150ms cubic-bezier(0.4, 0, 0.2, 1),
opacity 250ms cubic-bezier(0.5, 0, 0.2, 1) 50ms;
}
</style>