90 lines
2.6 KiB
Vue
90 lines
2.6 KiB
Vue
<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>
|