Files
kestrelos/app/components/NavDrawer.vue
Madison Grubb b7046dc0e6 initial commit
2026-02-10 23:32:26 -05:00

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">&times;</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>