Files
KanbanGit/resources/js/components/AppHeader.vue
T
Twyco a46695c2eb
linter / quality (pull_request) Successful in 1m18s
tests / ci (8.4) (pull_request) Successful in 1m54s
Implement Fluid Emerald app redesign
2026-07-18 16:45:54 +02:00

208 lines
7.0 KiB
Vue

<script setup lang="ts">
import { Link, usePage } from '@inertiajs/vue3';
import {
Folder,
LayoutGrid,
Layers3,
Menu,
Shield,
Users,
} from 'lucide-vue-next';
import { computed } from 'vue';
import AppLogo from '@/components/AppLogo.vue';
import Breadcrumbs from '@/components/Breadcrumbs.vue';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@/components/ui/sheet';
import UserAvatar from '@/components/ui/avatar/UserAvatar.vue';
import UserMenuContent from '@/components/UserMenuContent.vue';
import { useCurrentUrl } from '@/composables/useCurrentUrl';
import { dashboard } from '@/routes';
import { index as adminUsers } from '@/routes/admin/users';
import { index as projectTemplatesIndex } from '@/routes/project-templates';
import { index as projectsIndex } from '@/routes/projects';
import { index as teamsIndex } from '@/routes/teams';
import type { AppHeaderProps, NavItem } from '@/types';
const props = withDefaults(defineProps<AppHeaderProps>(), {
breadcrumbs: () => [],
});
const page = usePage();
const auth = computed(() => page.props.auth);
const { isCurrentOrParentUrl } = useCurrentUrl();
const mainNavItems = computed<NavItem[]>(() => {
const items: NavItem[] = [
{
title: 'Dashboard',
href: dashboard(),
icon: LayoutGrid,
},
{
title: 'Projects',
href: projectsIndex(),
icon: Folder,
},
];
if (page.props.auth.permissions.projectCreate) {
items.push({
title: 'Project templates',
href: projectTemplatesIndex(),
icon: Layers3,
});
}
if (page.props.auth.permissions.globalAdmin) {
items.push({
title: 'Users',
href: adminUsers(),
icon: Shield,
});
}
if (page.props.auth.permissions.canManageTeams) {
items.push({
title: 'Teams',
href: teamsIndex(),
icon: Users,
});
}
return items;
});
const navItemClasses = (item: NavItem): string =>
[
'inline-flex h-9 items-center gap-2 rounded-lg px-3 text-sm font-medium transition-colors duration-200',
isCurrentOrParentUrl(item.href)
? 'bg-primary text-primary-foreground shadow-[0_8px_20px_rgba(0,110,47,0.16)]'
: 'text-on-surface-variant hover:bg-surface-container-high hover:text-on-surface',
].join(' ');
</script>
<template>
<header
class="border-outline-variant/80 bg-surface/90 shadow-kanban-nav sticky top-0 z-50 border-b backdrop-blur-md"
>
<div
class="sm:px-container-margin flex h-16 w-full items-center gap-3 px-4"
>
<Sheet>
<SheetTrigger :as-child="true">
<Button
variant="ghost"
size="icon"
class="text-on-surface-variant hover:bg-surface-container-high hover:text-on-surface size-9 rounded-lg lg:hidden"
>
<Menu class="size-5" />
<span class="sr-only">Open navigation</span>
</Button>
</SheetTrigger>
<SheetContent
side="left"
class="border-outline-variant bg-surface w-[320px] p-0"
>
<SheetTitle class="sr-only">Navigation menu</SheetTitle>
<SheetHeader class="border-outline-variant border-b p-4">
<Link
:href="dashboard()"
class="flex items-center gap-3"
>
<AppLogo />
</Link>
</SheetHeader>
<div class="flex flex-col gap-5 p-4">
<nav class="flex flex-col gap-1" aria-label="Main">
<Link
v-for="item in mainNavItems"
:key="item.title"
:href="item.href"
:class="navItemClasses(item)"
>
<component
v-if="item.icon"
:is="item.icon"
class="size-4"
/>
{{ item.title }}
</Link>
</nav>
</div>
</SheetContent>
</Sheet>
<Link
:href="dashboard()"
class="flex shrink-0 items-center gap-2"
aria-label="KanbanGit dashboard"
>
<AppLogo />
</Link>
<nav
class="ml-3 hidden min-w-0 flex-1 items-center gap-1 lg:flex"
aria-label="Main"
>
<Link
v-for="item in mainNavItems"
:key="item.title"
:href="item.href"
:class="navItemClasses(item)"
>
<component
v-if="item.icon"
:is="item.icon"
class="size-4"
/>
<span class="truncate">{{ item.title }}</span>
</Link>
</nav>
<div class="ml-auto flex items-center gap-3">
<DropdownMenu>
<DropdownMenuTrigger :as-child="true">
<Button
variant="ghost"
size="icon"
class="focus-visible:ring-primary/30 hover:bg-surface-container-high size-10 rounded-full p-1 focus-visible:ring-2"
>
<UserAvatar rounded />
<span class="sr-only">Open user menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
class="border-outline-variant bg-popover w-56"
>
<UserMenuContent :user="auth.user" />
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<div
v-if="props.breadcrumbs.length > 1"
class="border-outline-variant/70 bg-surface-container-low/70 border-t"
>
<div
class="text-on-surface-variant sm:px-container-margin flex h-11 w-full items-center px-4 text-sm"
>
<Breadcrumbs :breadcrumbs="breadcrumbs" />
</div>
</div>
</header>
</template>