126 lines
3.4 KiB
Vue
126 lines
3.4 KiB
Vue
<template>
|
|
<Teleport to="body">
|
|
<Transition name="drawer-backdrop">
|
|
<button
|
|
v-if="modelValue"
|
|
type="button"
|
|
class="fixed inset-0 z-20 block h-full w-full border-0 bg-black/50 p-0 md:hidden"
|
|
aria-label="Close navigation"
|
|
@click="close"
|
|
/>
|
|
</Transition>
|
|
<aside
|
|
class="nav-drawer fixed left-0 top-0 z-30 flex h-full w-[260px] flex-col border-r border-kestrel-border bg-kestrel-surface transition-transform duration-200 ease-out"
|
|
:class="{ '-translate-x-full': !modelValue }"
|
|
role="navigation"
|
|
aria-label="Main navigation"
|
|
:aria-expanded="modelValue"
|
|
>
|
|
<div
|
|
class="flex h-14 shrink-0 items-center justify-between border-b border-kestrel-border bg-kestrel-surface px-4 shadow-glow-sm [box-shadow:0_0_20px_-4px_rgba(34,201,201,0.15)]"
|
|
>
|
|
<h2 class="text-sm font-medium uppercase tracking-wider text-kestrel-muted">
|
|
Navigation
|
|
</h2>
|
|
<button
|
|
type="button"
|
|
class="rounded p-1 text-kestrel-muted transition-colors hover:bg-kestrel-border hover:text-kestrel-accent"
|
|
aria-label="Close navigation"
|
|
@click="close"
|
|
>
|
|
<span class="text-xl leading-none">×</span>
|
|
</button>
|
|
</div>
|
|
<nav class="flex-1 overflow-auto py-2">
|
|
<ul class="space-y-0.5 px-2">
|
|
<li
|
|
v-for="item in navItems"
|
|
:key="item.to"
|
|
>
|
|
<NuxtLink
|
|
:to="item.to"
|
|
class="block rounded px-3 py-2 text-sm transition-colors"
|
|
:class="isActive(item.to)
|
|
? 'border-l-2 border-kestrel-accent bg-kestrel-surface-hover font-medium text-kestrel-accent [text-shadow:0_0_8px_rgba(34,201,201,0.25)]'
|
|
: 'border-l-2 border-transparent text-kestrel-muted hover:bg-kestrel-border hover:text-kestrel-text'"
|
|
@click="close"
|
|
>
|
|
{{ item.label }}
|
|
</NuxtLink>
|
|
</li>
|
|
</ul>
|
|
</nav>
|
|
</aside>
|
|
</Teleport>
|
|
</template>
|
|
|
|
<script setup>
|
|
defineProps({
|
|
modelValue: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
})
|
|
|
|
const emit = defineEmits(['update:modelValue'])
|
|
|
|
const route = useRoute()
|
|
const { canEditPois } = useUser()
|
|
|
|
const navItems = computed(() => {
|
|
const items = [
|
|
{ to: '/', label: 'Map' },
|
|
{ to: '/account', label: 'Account' },
|
|
{ to: '/cameras', label: 'Cameras' },
|
|
{ to: '/poi', label: 'POI' },
|
|
{ to: '/members', label: 'Members' },
|
|
{ to: '/settings', label: 'Settings' },
|
|
]
|
|
if (canEditPois.value) {
|
|
items.splice(1, 0, { to: '/share-live', label: 'Share live' })
|
|
}
|
|
return items
|
|
})
|
|
|
|
const isActive = to => to === '/' ? route.path === '/' : route.path.startsWith(to)
|
|
|
|
function close() {
|
|
emit('update:modelValue', false)
|
|
}
|
|
|
|
function onEscape(e) {
|
|
if (e.key === 'Escape') close()
|
|
}
|
|
|
|
defineExpose({ close })
|
|
|
|
onMounted(() => {
|
|
document.addEventListener('keydown', onEscape)
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
document.removeEventListener('keydown', onEscape)
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.drawer-backdrop-enter-active,
|
|
.drawer-backdrop-leave-active {
|
|
transition: opacity 0.2s ease;
|
|
}
|
|
.drawer-backdrop-enter-from,
|
|
.drawer-backdrop-leave-to {
|
|
opacity: 0;
|
|
}
|
|
|
|
/* Same elevation as content: no right-edge shadow on desktop so drawer and navbar read as one layer */
|
|
.nav-drawer {
|
|
box-shadow: 8px 0 24px -4px rgba(34, 201, 201, 0.12);
|
|
}
|
|
@media (min-width: 768px) {
|
|
.nav-drawer {
|
|
box-shadow: none;
|
|
}
|
|
}
|
|
</style>
|