268 lines
7.1 KiB
Vue
268 lines
7.1 KiB
Vue
<script setup lang="ts">
|
|
import { computed, nextTick, ref, watch } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { useSessionStore } from '@/stores/session'
|
|
import { useUpdateStore } from '@/stores/update'
|
|
import { useApi } from '@/composables/useApi'
|
|
import { useSidebarCollapsed } from '@/composables/useSidebarCollapsed'
|
|
import AppBoot from '@/components/AppBoot.vue'
|
|
import AccountMenuSheet from '@/components/AccountMenuSheet.vue'
|
|
import AppLogo from '@/components/AppLogo.vue'
|
|
import ErrorView from '@/components/ErrorView.vue'
|
|
import OfflineBanner from '@/components/OfflineBanner.vue'
|
|
import UpdatePill from '@/components/UpdatePill.vue'
|
|
import UserMenu from '@/components/UserMenu.vue'
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
mobileTitle?: string
|
|
fullBleed?: boolean
|
|
compactPadding?: boolean
|
|
renderWhileChecking?: boolean
|
|
offlineMessage?: string
|
|
offlineIcon?: string
|
|
}>(),
|
|
{
|
|
mobileTitle: undefined,
|
|
offlineMessage: undefined,
|
|
offlineIcon: undefined,
|
|
fullBleed: false,
|
|
compactPadding: false,
|
|
renderWhileChecking: false,
|
|
},
|
|
)
|
|
|
|
const COMPACT_BODY = 'gap-0 px-4 pt-4 pb-22 sm:gap-0 sm:px-4 sm:pt-4 sm:pb-22 lg:p-6'
|
|
const DEFAULT_BODY = 'gap-0 px-4 pt-5 pb-22 sm:gap-0 sm:px-4 sm:pt-5 sm:pb-22 lg:p-8'
|
|
|
|
const bodyUi = computed(() => {
|
|
if (props.fullBleed) {
|
|
return 'gap-0 p-0 sm:gap-0 sm:p-0 lg:p-0'
|
|
}
|
|
return props.compactPadding ? COMPACT_BODY : DEFAULT_BODY
|
|
})
|
|
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const session = useSessionStore()
|
|
const update = useUpdateStore()
|
|
const api = useApi()
|
|
|
|
const navItems = computed(() =>
|
|
[
|
|
{ label: 'Home', to: '/home', icon: 'i-lucide-home' },
|
|
{ label: 'Articles', to: '/article', icon: 'i-lucide-file-text' },
|
|
{ label: 'Media', to: '/media', icon: 'i-lucide-images' },
|
|
{ label: 'Users', to: '/users', icon: 'i-lucide-users' },
|
|
{ label: 'Integration', to: '/integration', icon: 'i-lucide-plug' },
|
|
{ label: 'Settings', to: '/settings', icon: 'i-lucide-settings', slot: 'settings' },
|
|
].map((item) => ({
|
|
...item,
|
|
active: route.path === item.to || route.path.startsWith(`${item.to}/`),
|
|
})),
|
|
)
|
|
|
|
const routeTitle = computed(() => (route.meta.title as string) ?? '')
|
|
const pageTitle = computed(() => props.mobileTitle ?? routeTitle.value)
|
|
const sidebarCollapsed = useSidebarCollapsed()
|
|
|
|
const sidebarUi = computed(() => ({
|
|
root: sidebarCollapsed.value ? 'min-w-14 px-1 py-3' : 'min-w-14 p-3',
|
|
header: 'mb-3 h-10 gap-2.5 px-1.5',
|
|
body: 'gap-0.5 p-0',
|
|
footer: 'p-0',
|
|
}))
|
|
|
|
const navUi = computed(() => ({
|
|
list: 'flex flex-col gap-0.5',
|
|
link: sidebarCollapsed.value
|
|
? 'h-8 justify-center gap-0 px-0 text-sm'
|
|
: 'h-8 gap-2.5 px-2.5 py-0 text-sm',
|
|
linkLeadingIcon: 'size-4',
|
|
}))
|
|
const loggingOut = ref(false)
|
|
const sidebarOpen = ref(false)
|
|
const accountSheetOpen = ref(false)
|
|
const NAVBAR_TOGGLE_SELECTOR = '[data-slot="toggle"]'
|
|
let sheetOpener: HTMLElement | undefined
|
|
|
|
function openAccountSheet(opener: HTMLElement | undefined) {
|
|
sheetOpener = opener
|
|
sidebarOpen.value = false
|
|
accountSheetOpen.value = true
|
|
}
|
|
|
|
async function restoreFocusAfterSheet() {
|
|
await nextTick()
|
|
if (loggingOut.value) {
|
|
return
|
|
}
|
|
const target = sheetOpener?.isConnected
|
|
? sheetOpener
|
|
: document.querySelector<HTMLElement>(NAVBAR_TOGGLE_SELECTOR)
|
|
sheetOpener = undefined
|
|
target?.focus()
|
|
}
|
|
|
|
watch(accountSheetOpen, (isOpen) => {
|
|
if (!isOpen) {
|
|
restoreFocusAfterSheet()
|
|
}
|
|
})
|
|
|
|
const showBoot = computed(() => session.status === 'checking' && !props.renderWhileChecking)
|
|
|
|
function onRetry() {
|
|
session.retry()
|
|
}
|
|
|
|
async function onLogout() {
|
|
loggingOut.value = true
|
|
try {
|
|
await api.request('/v1/auth/logout', { method: 'POST' })
|
|
} finally {
|
|
loggingOut.value = false
|
|
}
|
|
session.clear()
|
|
await router.push('/login')
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<UDashboardGroup unit="rem">
|
|
<a href="#dashboard-content" class="sr-only focus:not-sr-only">Skip to content</a>
|
|
|
|
<UDashboardSidebar
|
|
v-model:open="sidebarOpen"
|
|
v-model:collapsed="sidebarCollapsed"
|
|
collapsible
|
|
:resizable="false"
|
|
:default-size="14"
|
|
:collapsed-size="3.5"
|
|
:ui="sidebarUi"
|
|
>
|
|
<template #header>
|
|
<AppLogo :size="26" />
|
|
<span
|
|
v-if="!sidebarCollapsed"
|
|
aria-hidden="true"
|
|
class="text-base font-semibold text-highlighted"
|
|
>RenewCMS</span
|
|
>
|
|
</template>
|
|
|
|
<UNavigationMenu
|
|
orientation="vertical"
|
|
:items="navItems"
|
|
:collapsed="sidebarCollapsed"
|
|
aria-label="Main"
|
|
:ui="navUi"
|
|
>
|
|
<template #settings-trailing>
|
|
<UpdatePill v-if="update.showCue && !sidebarCollapsed" />
|
|
</template>
|
|
</UNavigationMenu>
|
|
|
|
<template #footer>
|
|
<UserMenu
|
|
:username="session.session?.username ?? ''"
|
|
:display-name="session.session?.displayName"
|
|
:email="session.session?.email"
|
|
:collapsed="sidebarCollapsed"
|
|
:sheet-open="accountSheetOpen"
|
|
@logout="onLogout"
|
|
@open-sheet="openAccountSheet"
|
|
/>
|
|
</template>
|
|
</UDashboardSidebar>
|
|
|
|
<UDashboardPanel :ui="{ body: bodyUi }">
|
|
<template #header>
|
|
<UDashboardNavbar
|
|
:title="pageTitle"
|
|
:ui="{
|
|
root: 'h-14 gap-1 bg-default px-1.5 sm:px-1.5 lg:h-12 lg:gap-2 lg:px-6',
|
|
left: 'min-w-0 flex-1 gap-1 lg:gap-2',
|
|
title: 'text-lg lg:sr-only',
|
|
toggle: 'size-11 justify-center',
|
|
}"
|
|
>
|
|
<template #toggle>
|
|
<span class="relative lg:hidden">
|
|
<UDashboardSidebarToggle
|
|
data-slot="toggle"
|
|
color="neutral"
|
|
variant="ghost"
|
|
class="size-11 justify-center"
|
|
:ui="{ leadingIcon: 'size-5' }"
|
|
/>
|
|
<span
|
|
v-if="update.showCue"
|
|
role="img"
|
|
aria-label="Update available"
|
|
class="absolute top-2.25 right-2.25 size-2.5 border-2 border-(--ui-bg) bg-info"
|
|
/>
|
|
</span>
|
|
</template>
|
|
<template #leading>
|
|
<UDashboardSidebarCollapse
|
|
color="neutral"
|
|
variant="ghost"
|
|
size="sm"
|
|
class="-mx-1.5 text-muted"
|
|
:aria-expanded="!sidebarCollapsed"
|
|
data-testid="sidebar-collapse"
|
|
/>
|
|
<span
|
|
aria-hidden="true"
|
|
class="mx-1.5 hidden h-4 w-px bg-(--ui-border) lg:block"
|
|
/>
|
|
<slot name="breadcrumb">
|
|
<UBreadcrumb
|
|
:items="[{ label: routeTitle }]"
|
|
color="neutral"
|
|
class="hidden lg:block"
|
|
:ui="{ link: 'text-sm font-medium' }"
|
|
/>
|
|
</slot>
|
|
</template>
|
|
<template #right>
|
|
<slot name="navbar-right" />
|
|
</template>
|
|
</UDashboardNavbar>
|
|
</template>
|
|
|
|
<template #body>
|
|
<div
|
|
id="dashboard-content"
|
|
:class="fullBleed ? 'flex h-full min-h-0 flex-1 flex-col' : ''"
|
|
>
|
|
<OfflineBanner
|
|
:message="offlineMessage"
|
|
:icon="offlineIcon"
|
|
:bleed="compactPadding"
|
|
/>
|
|
|
|
<AppBoot v-if="showBoot" />
|
|
<ErrorView
|
|
v-else-if="session.status === 'error'"
|
|
kind="error"
|
|
@retry="onRetry"
|
|
/>
|
|
<div v-else :class="fullBleed ? 'flex min-h-0 flex-1 flex-col' : ''">
|
|
<slot />
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</UDashboardPanel>
|
|
|
|
<AccountMenuSheet
|
|
v-model:open="accountSheetOpen"
|
|
:username="session.session?.username ?? ''"
|
|
:display-name="session.session?.displayName"
|
|
:email="session.session?.email"
|
|
@logout="onLogout"
|
|
/>
|
|
</UDashboardGroup>
|
|
</template>
|