Files
KanbanGit/resources/js/components/AppHeader.vue
T
Twyco 59817ad30f
linter / quality (pull_request) Successful in 2m0s
tests / ci (8.4) (pull_request) Successful in 1m57s
Redesign app layout chrome
2026-06-25 01:56:54 +02:00

240 lines
8.5 KiB
Vue

<script setup lang="ts">
import { Link, usePage } from '@inertiajs/vue3';
import {
Folder,
LayoutGrid,
Layers3,
Menu,
Search,
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 { BreadcrumbItem, NavItem } from '@/types';
const props = withDefaults(
defineProps<{
breadcrumbs?: BreadcrumbItem[];
}>(),
{
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">
<div class="relative">
<Search
class="text-on-surface-variant pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2"
/>
<input
disabled
aria-label="Search"
placeholder="Coming soon"
class="border-outline-variant bg-surface-container-low text-on-surface placeholder:text-on-surface-variant h-10 w-full rounded-lg border py-2 pl-9 pr-3 text-sm disabled:cursor-not-allowed disabled:opacity-100"
type="text"
/>
</div>
<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">
<div class="relative hidden sm:block">
<Search
class="text-on-surface-variant pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2"
/>
<input
disabled
aria-label="Search"
placeholder="Coming soon"
class="border-outline-variant bg-surface-container-low text-on-surface placeholder:text-on-surface-variant h-10 w-48 rounded-lg border py-2 pl-9 pr-3 text-sm transition-colors disabled:cursor-not-allowed disabled:opacity-100 md:w-64"
type="text"
/>
</div>
<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>