new nav system
All checks were successful
ci/woodpecker/pr/pr Pipeline was successful

This commit is contained in:
Madison Grubb
2026-02-14 22:47:05 -05:00
parent 9261ba92bf
commit 4e51ca5509
27 changed files with 1198 additions and 688 deletions

View File

@@ -1,8 +1,8 @@
<template>
<Teleport to="body">
<div class="flex h-full shrink-0">
<Transition name="drawer-backdrop">
<button
v-if="modelValue"
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"
@@ -10,18 +10,19 @@
/>
</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 }"
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
class="flex h-14 shrink-0 items-center justify-between border-b border-kestrel-border bg-kestrel-surface px-4 shadow-glow-sm shadow-glow-header"
v-if="isMounted && isMobile"
class="flex shrink-0 items-center justify-end border-b border-kestrel-border bg-kestrel-surface px-2 py-1"
>
<h2 class="text-sm font-medium uppercase tracking-wider text-kestrel-muted">
Navigation
</h2>
<button
type="button"
class="kestrel-close-btn"
@@ -31,7 +32,7 @@
<span class="text-xl leading-none">&times;</span>
</button>
</div>
<nav class="flex-1 overflow-auto py-2">
<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"
@@ -39,50 +40,91 @@
>
<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-glow-sm'
: 'border-l-2 border-transparent text-kestrel-muted hover:bg-kestrel-border hover:text-kestrel-text'"
@click="close"
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"
>
{{ item.label }}
<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>
</Teleport>
</div>
</template>
<script setup>
defineProps({
modelValue: {
type: Boolean,
default: false,
},
const props = defineProps({
modelValue: { type: Boolean, default: false },
collapsed: { type: Boolean, default: false },
isMobile: { type: Boolean, default: true },
})
const emit = defineEmits(['update:modelValue'])
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(() => {
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
if (!canEditPois.value) return NAV_ITEMS
const list = [...NAV_ITEMS]
list.splice(3, 0, SHARE_LIVE_ITEM)
return list
})
const isActive = to => to === '/' ? route.path === '/' : route.path.startsWith(to)
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)
@@ -95,6 +137,7 @@ function onEscape(e) {
defineExpose({ close })
onMounted(() => {
isMounted.value = true
document.addEventListener('keydown', onEscape)
})