148 lines
4.7 KiB
Vue
148 lines
4.7 KiB
Vue
<template>
|
|
<div class="flex h-full shrink-0">
|
|
<Transition name="drawer-backdrop">
|
|
<button
|
|
v-if="isMobile && 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 flex h-full flex-col bg-kestrel-surface transition-[width] duration-200 ease-out md:relative md:translate-x-0"
|
|
:class="[
|
|
isMobile && !modelValue ? 'fixed left-0 top-14 z-30 -translate-x-full' : 'fixed left-0 top-14 z-30 md:relative md:top-0',
|
|
showCollapsed ? 'w-16' : 'w-[260px]',
|
|
]"
|
|
role="navigation"
|
|
aria-label="Main navigation"
|
|
:aria-expanded="modelValue"
|
|
>
|
|
<div
|
|
v-if="isMounted && isMobile"
|
|
class="flex shrink-0 items-center justify-end border-b border-kestrel-border bg-kestrel-surface px-2 py-1"
|
|
>
|
|
<button
|
|
type="button"
|
|
class="kestrel-close-btn"
|
|
aria-label="Close navigation"
|
|
@click="close"
|
|
>
|
|
<span class="text-xl leading-none">×</span>
|
|
</button>
|
|
</div>
|
|
<nav class="flex-1 overflow-auto bg-kestrel-surface py-2">
|
|
<ul class="space-y-0.5 px-2">
|
|
<li
|
|
v-for="item in navItems"
|
|
:key="item.to"
|
|
>
|
|
<NuxtLink
|
|
:to="item.to"
|
|
class="flex items-center gap-3 rounded px-3 py-2 text-sm transition-colors"
|
|
:class="[
|
|
showCollapsed ? 'justify-center px-2' : '',
|
|
isActive(item.to)
|
|
? 'bg-kestrel-surface-hover font-medium text-kestrel-accent text-shadow-glow-sm'
|
|
: 'text-kestrel-muted hover:bg-kestrel-border hover:text-kestrel-text',
|
|
!showCollapsed && (isActive(item.to) ? 'border-l-2 border-kestrel-accent' : 'border-l-2 border-transparent'),
|
|
]"
|
|
:title="showCollapsed ? item.label : undefined"
|
|
@click="isMobile ? close() : undefined"
|
|
>
|
|
<Icon
|
|
:name="item.icon"
|
|
class="size-5 shrink-0"
|
|
aria-hidden="true"
|
|
/>
|
|
<span
|
|
v-show="!showCollapsed"
|
|
class="truncate"
|
|
>{{ item.label }}</span>
|
|
</NuxtLink>
|
|
</li>
|
|
</ul>
|
|
</nav>
|
|
<div
|
|
v-if="isMounted && !isMobile"
|
|
class="shrink-0 border-t border-kestrel-border bg-kestrel-surface py-2"
|
|
>
|
|
<button
|
|
type="button"
|
|
class="flex w-full items-center gap-3 rounded px-3 py-2 text-sm text-kestrel-muted transition-colors hover:bg-kestrel-border hover:text-kestrel-text"
|
|
:class="showCollapsed ? 'justify-center px-2' : ''"
|
|
:aria-label="showCollapsed ? 'Expand sidebar' : 'Collapse sidebar'"
|
|
@click="toggleCollapsed"
|
|
>
|
|
<Icon
|
|
:name="showCollapsed ? 'tabler:chevron-right' : 'tabler:chevron-left'"
|
|
class="size-5 shrink-0"
|
|
aria-hidden="true"
|
|
/>
|
|
<span v-show="!showCollapsed">Collapse sidebar</span>
|
|
</button>
|
|
</div>
|
|
</aside>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
const props = defineProps({
|
|
modelValue: { type: Boolean, default: false },
|
|
collapsed: { type: Boolean, default: false },
|
|
isMobile: { type: Boolean, default: true },
|
|
})
|
|
|
|
const emit = defineEmits(['update:modelValue', 'update:collapsed'])
|
|
|
|
const isMounted = ref(false)
|
|
const route = useRoute()
|
|
const { canEditPois } = useUser()
|
|
|
|
const NAV_ITEMS = Object.freeze([
|
|
{ to: '/', label: 'Map', icon: 'tabler:map' },
|
|
{ to: '/cameras', label: 'Cameras', icon: 'tabler:video' },
|
|
{ to: '/poi', label: 'POI', icon: 'tabler:map-pin' },
|
|
{ to: '/members', label: 'Members', icon: 'tabler:users' },
|
|
{ to: '/account', label: 'Account', icon: 'tabler:user-circle' },
|
|
{ to: '/settings', label: 'Settings', icon: 'tabler:settings' },
|
|
])
|
|
|
|
const SHARE_LIVE_ITEM = { to: '/share-live', label: 'Share live', icon: 'tabler:live-photo' }
|
|
|
|
const navItems = computed(() => {
|
|
if (!canEditPois.value) return NAV_ITEMS
|
|
const list = [...NAV_ITEMS]
|
|
list.splice(3, 0, SHARE_LIVE_ITEM)
|
|
return list
|
|
})
|
|
|
|
const showCollapsed = computed(() => props.collapsed && !props.isMobile)
|
|
|
|
function toggleCollapsed() {
|
|
emit('update:collapsed', !props.collapsed)
|
|
}
|
|
|
|
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(() => {
|
|
isMounted.value = true
|
|
document.addEventListener('keydown', onEscape)
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
document.removeEventListener('keydown', onEscape)
|
|
})
|
|
</script>
|