feat(KAN-19): add responsive permission-aware navigation
This commit is contained in:
@@ -1,27 +1,145 @@
|
||||
<script setup lang="ts">
|
||||
import { Link, usePage } from '@inertiajs/vue3';
|
||||
import { Monitor, Moon, Plus, Sun } from 'lucide-vue-next';
|
||||
import { computed } from 'vue';
|
||||
import Breadcrumbs from '@/components/Breadcrumbs.vue';
|
||||
import UserMenuContent from '@/components/UserMenuContent.vue';
|
||||
import { UserAvatar } from '@/components/ui/avatar';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { SidebarTrigger } from '@/components/ui/sidebar';
|
||||
import { useAppNavigation } from '@/composables/useAppNavigation';
|
||||
import {
|
||||
normalizeAppearance,
|
||||
useAppearance,
|
||||
} from '@/composables/useAppearance';
|
||||
import { useCurrentUrl } from '@/composables/useCurrentUrl';
|
||||
import { useLocalization } from '@/composables/useLocalization';
|
||||
import { create as createProject } from '@/routes/projects';
|
||||
import { index as board } from '@/routes/projects/board';
|
||||
import { create as createTicket } from '@/routes/projects/tickets';
|
||||
import type { BreadcrumbItem } from '@/types';
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
breadcrumbs?: BreadcrumbItem[];
|
||||
}>(),
|
||||
{
|
||||
breadcrumbs: () => [],
|
||||
},
|
||||
withDefaults(defineProps<{ breadcrumbs?: BreadcrumbItem[] }>(), {
|
||||
breadcrumbs: () => [],
|
||||
});
|
||||
const page = usePage();
|
||||
const { project } = useAppNavigation();
|
||||
const { isCurrentUrl } = useCurrentUrl();
|
||||
const { t } = useLocalization();
|
||||
const { appearance, updateAppearance } = useAppearance();
|
||||
const themeIcon = computed(
|
||||
() => ({ light: Sun, dark: Moon, system: Monitor })[appearance.value],
|
||||
);
|
||||
const createAction = computed(() => {
|
||||
if (project.value?.canCreateTickets) {
|
||||
return {
|
||||
title: t('navigation.createTicket'),
|
||||
href: createTicket(project.value.id, {
|
||||
query: {
|
||||
source: isCurrentUrl(board(project.value.id))
|
||||
? 'board'
|
||||
: 'backlog',
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
if (!project.value && page.props.auth.permissions.projectCreate) {
|
||||
return { title: t('navigation.createProject'), href: createProject() };
|
||||
}
|
||||
return null;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header
|
||||
class="border-sidebar-border/70 group-has-data-[collapsible=icon]/sidebar-wrapper:h-12 flex h-16 shrink-0 items-center gap-2 border-b px-6 transition-[width,height] ease-linear md:px-4"
|
||||
class="bg-background md:px-page sticky top-0 z-20 flex min-h-16 min-w-0 shrink-0 items-center justify-between gap-2 border-b px-3 md:gap-4"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<SidebarTrigger class="-ml-1" />
|
||||
<template v-if="breadcrumbs && breadcrumbs.length > 0">
|
||||
<Breadcrumbs :breadcrumbs="breadcrumbs" />
|
||||
</template>
|
||||
<div class="flex min-w-0 flex-1 items-center gap-2">
|
||||
<SidebarTrigger class="shrink-0" />
|
||||
<Breadcrumbs v-if="breadcrumbs.length" :breadcrumbs="breadcrumbs" />
|
||||
<span v-else class="truncate font-semibold">KanbanGit</span>
|
||||
</div>
|
||||
<div class="flex shrink-0 items-center gap-1 sm:gap-2">
|
||||
<Button
|
||||
v-if="createAction"
|
||||
as-child
|
||||
:aria-label="createAction.title"
|
||||
class="px-3"
|
||||
>
|
||||
<Link :href="createAction.href"
|
||||
><Plus aria-hidden="true" /><span
|
||||
class="hidden sm:inline"
|
||||
>{{ createAction.title }}</span
|
||||
></Link
|
||||
>
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
:aria-label="t('appearance.label')"
|
||||
><component :is="themeIcon" aria-hidden="true"
|
||||
/></Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>{{
|
||||
t('appearance.label')
|
||||
}}</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuRadioGroup
|
||||
:model-value="appearance"
|
||||
@update:model-value="
|
||||
(value) =>
|
||||
updateAppearance(normalizeAppearance(value))
|
||||
"
|
||||
>
|
||||
<DropdownMenuRadioItem value="light"
|
||||
><Sun aria-hidden="true" />{{
|
||||
t('appearance.light')
|
||||
}}</DropdownMenuRadioItem
|
||||
>
|
||||
<DropdownMenuRadioItem value="dark"
|
||||
><Moon aria-hidden="true" />{{
|
||||
t('appearance.dark')
|
||||
}}</DropdownMenuRadioItem
|
||||
>
|
||||
<DropdownMenuRadioItem value="system"
|
||||
><Monitor aria-hidden="true" />{{
|
||||
t('appearance.system')
|
||||
}}</DropdownMenuRadioItem
|
||||
>
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
:aria-label="
|
||||
t('navigation.userMenu', {
|
||||
name: page.props.auth.user.name,
|
||||
})
|
||||
"
|
||||
data-test="sidebar-menu-button"
|
||||
>
|
||||
<UserAvatar :user="page.props.auth.user" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" class="min-w-60">
|
||||
<UserMenuContent :user="page.props.auth.user" />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user