minor: new nav system (#5)
All checks were successful
ci/woodpecker/push/push Pipeline was successful
All checks were successful
ci/woodpecker/push/push Pipeline was successful
Co-authored-by: Madison Grubb <madison@elastiflow.com> Reviewed-on: #5
This commit was merged in pull request #5.
This commit is contained in:
89
app/components/AppShell.vue
Normal file
89
app/components/AppShell.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<div class="flex min-h-0 flex-1 flex-col">
|
||||
<header class="relative z-40 flex h-14 shrink-0 items-center gap-3 bg-kestrel-surface px-4">
|
||||
<NuxtLink
|
||||
to="/"
|
||||
class="text-lg font-semibold tracking-wide text-kestrel-text no-underline text-shadow-glow-md transition-colors hover:text-kestrel-accent focus-visible:ring-2 focus-visible:ring-kestrel-accent focus-visible:rounded"
|
||||
>
|
||||
KestrelOS
|
||||
</NuxtLink>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded p-2 text-kestrel-muted transition-colors hover:bg-kestrel-border hover:text-kestrel-accent md:hidden"
|
||||
aria-label="Toggle navigation"
|
||||
:aria-expanded="drawerOpen"
|
||||
@click="drawerOpen = !drawerOpen"
|
||||
>
|
||||
<span
|
||||
class="text-lg leading-none"
|
||||
aria-hidden="true"
|
||||
>☰</span>
|
||||
</button>
|
||||
<div class="min-w-0 flex-1" />
|
||||
<div class="flex items-center gap-2">
|
||||
<UserMenu
|
||||
v-if="user"
|
||||
:user="user"
|
||||
@signout="onLogout"
|
||||
/>
|
||||
<NuxtLink
|
||||
v-else
|
||||
to="/login"
|
||||
class="rounded px-2 py-1 text-xs text-kestrel-muted hover:bg-kestrel-border hover:text-kestrel-accent"
|
||||
>
|
||||
Sign in
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</header>
|
||||
<div class="flex min-h-0 flex-1">
|
||||
<NavDrawer
|
||||
v-model="drawerOpen"
|
||||
v-model:collapsed="sidebarCollapsed"
|
||||
:is-mobile="isMobile"
|
||||
/>
|
||||
<!-- Content area: rounded top-left so it nestles into the shell (GitLab gl-rounded-t-lg style). -->
|
||||
<div class="relative min-h-0 flex-1 min-w-0 overflow-clip rounded-tl-lg">
|
||||
<main class="relative h-full w-full min-h-0 overflow-auto">
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const isMobile = useMediaQuery('(max-width: 767px)')
|
||||
const drawerOpen = ref(true)
|
||||
|
||||
const SIDEBAR_COLLAPSED_KEY = 'kestrelos-sidebar-collapsed'
|
||||
const sidebarCollapsed = ref(false)
|
||||
onMounted(() => {
|
||||
try {
|
||||
const stored = localStorage.getItem(SIDEBAR_COLLAPSED_KEY)
|
||||
if (stored !== null) sidebarCollapsed.value = stored === 'true'
|
||||
}
|
||||
catch {
|
||||
// localStorage unavailable (e.g. private mode)
|
||||
}
|
||||
})
|
||||
watch(sidebarCollapsed, (v) => {
|
||||
try {
|
||||
localStorage.setItem(SIDEBAR_COLLAPSED_KEY, String(v))
|
||||
}
|
||||
catch {
|
||||
// localStorage unavailable
|
||||
}
|
||||
})
|
||||
|
||||
const { user, refresh } = useUser()
|
||||
|
||||
watch(isMobile, (mobile) => {
|
||||
if (mobile) drawerOpen.value = false
|
||||
}, { immediate: true })
|
||||
|
||||
async function onLogout() {
|
||||
await $fetch('/api/auth/logout', { method: 'POST' })
|
||||
await refresh()
|
||||
await navigateTo('/')
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user