Files
My-KOPKB/fe/src/themes/Enigma/SideMenu/SideMenu.vue
T

182 lines
11 KiB
Vue

<script setup lang="ts">
import { onMounted } from 'vue'
import { useBreadcrumb } from '@/composables/useBreadcrumb'
import { useNotifications } from '@/modules/notification'
import '@/assets/css/themes/enigma/side-menu.css'
import logo from '@/assets/images/logo-kopkb.svg'
import { useSideMenu } from '@/composables/useSideMenu'
import { useAuthStore } from '@/stores/auth'
import { Lucide } from '@/components/ui/lucide'
import { Breadcrumb } from '@/components/ui/breadcrumb'
import { AvatarRoot, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import mainMenu from '@/main/side-menu'
import { useFilteredMenu } from '@/composables/useFilteredMenu'
import { SideMenu } from '@/components/side-menu'
import { AccountDropdown, AccountTrigger } from '@/components/account-dropdown'
import { NotificationDropdown } from '@/components/notification-dropdown'
import {
ScrollAreaRoot,
ScrollAreaViewport,
ScrollAreaContent,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaCorner,
} from "@/components/ui/scroll-area";
const {
mobileMenuOpen,
scrolled,
toggleCompactMenu,
openMobileMenu,
closeMobileMenu,
onScrollContent,
} = useSideMenu()
const authStore = useAuthStore()
const breadcrumbItems = useBreadcrumb([])
const filteredMenu = useFilteredMenu(mainMenu)
const { unreadCount, fetchNotifications } = useNotifications()
const appName = import.meta.env.VITE_APP_NAME
const appVersion = import.meta.env.VITE_APP_VERSION
onMounted(() => {
authStore.fetchSession()
fetchNotifications()
})
</script>
<template>
<div :class="[
'enigma',
'min-h-screen bg-[color-mix(in_oklch,var(--color-background),var(--color-foreground)_3%)] dark:bg-background',
'before:bg-noise dark:before:bg-foreground/1 before:fixed before:inset-0 before:opacity-20',
'after:bg-accent after:bg-contain after:fixed after:inset-0 after:blur-xl after:opacity-[.25]',
]">
<!-- LEFT SIDE MENU (drawer/sidebar) -->
<div :class="[
'side-menu xl:ml-0 transition-[margin] duration-100 fixed top-0 left-0 z-50 group',
'before:content-[\'\'] before:fixed before:inset-0 before:bg-black/80 dark:before:bg-foreground/5 before:backdrop-blur before:xl:hidden',
'after:content-[\'\'] after:absolute after:inset-0 after:bg-background after:xl:hidden',
'[&.side-menu--mobile-menu-open]:ml-0 [&.side-menu--mobile-menu-open]:before:block',
'ml-[-320px] before:hidden',
{ 'side-menu--mobile-menu-open': mobileMenuOpen },
]">
<div @click="closeMobileMenu" :class="[
'close-mobile-menu fixed ml-[320px] xl:hidden z-50 cursor-pointer text-background dark:text-foreground hidden',
'[&.close-mobile-menu--mobile-menu-open]:block',
{ 'close-mobile-menu--mobile-menu-open': mobileMenuOpen },
]">
<div class="ml-5 mt-5 flex size-10 items-center justify-center">
<Lucide class="size-7 stroke-1" icon="X" />
</div>
</div>
<div :class="[
'side-menu__content',
'z-20 relative w-[320px] duration-100 transition-[width] group-[.side-menu--collapsed]:xl:w-[165px] group-[.side-menu--collapsed]:group-hover:xl:w-[320px] h-screen flex flex-col-reverse xl:flex-col',
'before:absolute before:inset-y-0 before:w-px before:mt-23 before:bg-foreground/11 dark:before:bg-foreground/10 before:right-0 before:mr-8 before:hidden xl:before:block',
]">
<div
class="relative z-10 mt-3 hidden h-[90px] w-[320px] flex-none items-center overflow-hidden pl-8 pr-14 duration-100 xl:flex group-[.side-menu--collapsed]:group-hover:xl:w-[320px] group-[.side-menu--collapsed]:xl:w-[165px]">
<a class="relative flex items-center transition-[margin] duration-100 xl:ml-1.5 group-[.side-menu--collapsed]:group-hover:xl:ml-1.5 group-[.side-menu--collapsed]:xl:ml-8"
href="">
<img class="size-12" :src="logo" />
<div
class="dark:text-foreground text-background ml-3.5 text-nowrap transition-opacity group-[.side-menu--collapsed]:group-hover:xl:opacity-100 group-[.side-menu--collapsed]:xl:opacity-0">
<span class="text-base font-medium">{{ appName }} {{ appVersion }}</span>
</div>
</a>
<button type="button" @click="toggleCompactMenu" aria-label="Toggle compact menu"
class="toggle-compact-menu text-background dark:text-foreground border-background/20 bg-background/10 dark:bg-foreground/2 dark:border-foreground/11 relative ml-auto hidden items-center justify-center rounded-md border py-0.5 pl-0.5 pr-1 opacity-70 transition-[opacity,transform] hover:opacity-100 group-[.side-menu--collapsed]:xl:rotate-180 group-[.side-menu--collapsed]:group-hover:xl:opacity-100 group-[.side-menu--collapsed]:xl:opacity-0 2xl:flex">
<Lucide icon="ChevronLeft" />
</button>
</div>
<AccountTrigger class="relative transition-[width] xl:mb-2 xl:mr-8"
innerClass="border-foreground/[.11] dark:border-foreground/[.11] border-t py-3 pl-6 pr-5 opacity-90 hover:opacity-100 xl:border-b xl:border-t-0 xl:pb-6 xl:pl-8 xl:pt-2.5"
avatarClass="h-11 w-11 border-background/70 dark:border-foreground/20 shadow-sm"
textClass="text-primary dark:text-foreground w-full">
<AccountDropdown class="absolute top-0 left-full origin-bottom-left"
boxClass="absolute bottom-0 left-[100%] ml-2 xl:bottom-auto xl:top-0" />
</AccountTrigger>
<ScrollAreaRoot class="flex-1 min-h-0">
<ScrollAreaViewport :class="[
'pl-4 xl:pl-7 pr-4 xl:pr-14 pb-3',
'[-webkit-mask-image:linear-gradient(to_top,rgba(0,0,0,0),black_30px),linear-gradient(to_bottom,rgba(0,0,0,0),black_30px)]',
'[-webkit-mask-composite:destination-in]',
].join(' ')">
<ScrollAreaContent class="min-w-auto!">
<SideMenu :menu="filteredMenu" />
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar class="bg-transparent xl:data-[orientation=vertical]:mr-9">
<ScrollAreaThumb class="bg-foreground/10" />
</ScrollAreaScrollbar>
<ScrollAreaCorner />
</ScrollAreaRoot>
</div>
</div>
<div :class="[
'content h-screen transition-[margin,width] duration-100 pt-32 pb-8 px-7 z-10 relative group',
'before:absolute before:bottom-4 before:top-27 before:-ml-px before:right-4 before:opacity-[.07] before:left-4 xl:before:left-0 before:bg-foreground before:rounded-4xl',
'after:absolute after:bottom-4 after:top-27 after:-ml-px after:right-4 after:left-4 xl:after:left-0 after:bg-[color-mix(in_oklch,var(--color-background),var(--color-foreground)_2%)] after:rounded-4xl after:border after:border-foreground/15 dark:after:opacity-[.59]',
'xl:ml-[320px]',
'[&.content--compact]:xl:ml-[165px]',
]">
<div :class="[
'relative h-full',
'[--color-nav-foreground:var(--color-background)] dark:[--color-nav-foreground:var(--color-foreground)]',
]">
<div class="h-full overflow-x-hidden">
<div @scroll="onScrollContent"
class="content__scroll-area relative z-20 -mr-7 h-full overflow-y-auto pl-4 pr-11 pb-5 pt-2 transition-[margin] duration-100 xl:pl-0">
<!-- TOP HEADER (top-bar: breadcrumb, role switch, notifications, profile) -->
<div :class="[
'top-bar group fixed inset-x-0 mt-3 top-0 z-50 pl-9 xl:pl-6 pr-9 xl:pr-5 transition-[margin,width] duration-100 xl:ml-[320px] xl:mr-5 group-[.content--compact]:xl:ml-[165px]',
'before:inset-y-0 before:left-4 before:transition-[margin] before:duration-100 before:right-0 before:absolute xl:before:ml-[-320px] group-[.content--compact]:xl:before:ml-[-165px] before:bg-primary before:border before:border-primary dark:before:border-[color-mix(in_oklch,var(--color-background),white_27%)] dark:before:bg-[color-mix(in_oklch,var(--color-background),white_14%)] before:my-3 before:rounded-2xl before:mr-4 xl:before:-mr-1',
'after:inset-0 after:left-8 after:transition-[margin] after:duration-100 after:right-0 after:absolute xl:after:ml-[-320px] group-[.content--compact]:xl:after:ml-[-165px] after:bg-primary/30 after:border after:border-primary/10 dark:after:border-[color-mix(in_oklch,var(--color-background),white_27%)] dark:after:bg-[color-mix(in_oklch,var(--color-background),white_14%)] after:z-[-1] after:mb-3 after:rounded-2xl after:mr-8 xl:after:mr-4',
{ scrolled: scrolled },
]">
<div :class="['relative z-20 flex h-[90px] items-center gap-5']">
<div @click="openMobileMenu"
class="open-mobile-menu bg-(--color-nav-foreground)/10 border-(--color-nav-foreground)/30 mr-auto flex size-9 cursor-pointer items-center justify-center rounded-xl border xl:hidden">
<Lucide class="rotate-90 [--color:var(--color-nav-foreground)]" icon="ChartNoAxesColumn" />
</div>
<Breadcrumb v-if="breadcrumbItems.length"
class="mr-auto hidden xl:flex [&_ol]:text-(--color-nav-foreground)/70 [&_li]:last:text-(--color-nav-foreground)/90 [&_li]:hover:text-(--color-nav-foreground)/90"
:items="breadcrumbItems" />
<div
class="group/notifications relative flex size-10 flex-none cursor-pointer items-center justify-center rounded-xl border border-(--color-nav-foreground)/30 bg-(--color-nav-foreground)/10 transition-colors hover:bg-(--color-nav-foreground)/20"
>
<Lucide
class="size-[22px] stroke-[1.75] [--color:var(--color-nav-foreground)]"
icon="Bell"
/>
<span
v-if="unreadCount > 0"
class="bg-danger text-background ring-background absolute -right-1.5 -top-1.5 flex min-w-5 items-center justify-center rounded-full px-1 py-0.5 text-[11px] font-semibold leading-none ring-2"
>
{{ unreadCount > 9 ? '9+' : unreadCount }}
</span>
<NotificationDropdown class="absolute right-0 top-full mt-2 origin-top-right"
boxClass="absolute right-0 top-0 -mr-0.5 -mt-0.5" />
</div>
<div class="group/profile relative size-9 flex-none">
<AvatarRoot class="ring-(--color)/40 size-full [--color:var(--color-nav-foreground)] rounded-full">
<AvatarFallback>{{ authStore.userName }}</AvatarFallback>
<AvatarImage v-if="authStore.userImageUrl" :src="authStore.userImageUrl"
:alt="authStore.userName" />
</AvatarRoot>
<AccountDropdown class="absolute right-0 top-full mt-2 origin-top-right"
boxClass="absolute right-0 top-0 -mr-0.5 -mt-0.5" />
</div>
</div>
</div>
<RouterView />
</div>
</div>
</div>
</div>
</div>
</template>