Redesign app layout chrome
This commit is contained in:
@@ -20,7 +20,7 @@ const className = computed(() => props.class);
|
||||
</SidebarInset>
|
||||
<main
|
||||
v-else
|
||||
class="mx-auto flex h-full w-full max-w-7xl flex-1 flex-col gap-4 rounded-xl"
|
||||
class="flex h-full w-full flex-1 flex-col gap-4"
|
||||
:class="className"
|
||||
>
|
||||
<slot />
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { Link, usePage } from '@inertiajs/vue3';
|
||||
import { BookOpen, Folder, LayoutGrid, Menu, Search } from 'lucide-vue-next';
|
||||
import {
|
||||
Folder,
|
||||
LayoutGrid,
|
||||
Layers3,
|
||||
Menu,
|
||||
Search,
|
||||
Shield,
|
||||
Users,
|
||||
} from 'lucide-vue-next';
|
||||
import { computed } from 'vue';
|
||||
import AppLogo from '@/components/AppLogo.vue';
|
||||
import AppLogoIcon from '@/components/AppLogoIcon.vue';
|
||||
import Breadcrumbs from '@/components/Breadcrumbs.vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
@@ -11,12 +18,6 @@ import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import {
|
||||
NavigationMenu,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuList,
|
||||
navigationMenuTriggerStyle,
|
||||
} from '@/components/ui/navigation-menu';
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
@@ -24,246 +25,215 @@ import {
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from '@/components/ui/sheet';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import UserAvatar from '@/components/ui/avatar/UserAvatar.vue';
|
||||
import UserMenuContent from '@/components/UserMenuContent.vue';
|
||||
import { useCurrentUrl } from '@/composables/useCurrentUrl';
|
||||
import { toUrl } from '@/lib/utils';
|
||||
import { dashboard } from '@/routes/index';
|
||||
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';
|
||||
import UserAvatar from '@/components/ui/avatar/UserAvatar.vue';
|
||||
|
||||
type Props = {
|
||||
breadcrumbs?: BreadcrumbItem[];
|
||||
};
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
breadcrumbs: () => [],
|
||||
});
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
breadcrumbs?: BreadcrumbItem[];
|
||||
}>(),
|
||||
{
|
||||
breadcrumbs: () => [],
|
||||
},
|
||||
);
|
||||
|
||||
const page = usePage();
|
||||
const auth = computed(() => page.props.auth);
|
||||
const { isCurrentUrl, whenCurrentUrl } = useCurrentUrl();
|
||||
const { isCurrentOrParentUrl } = useCurrentUrl();
|
||||
|
||||
const activeItemStyles =
|
||||
'text-neutral-900 dark:bg-neutral-800 dark:text-neutral-100';
|
||||
const mainNavItems = computed<NavItem[]>(() => {
|
||||
const items: NavItem[] = [
|
||||
{
|
||||
title: 'Dashboard',
|
||||
href: dashboard(),
|
||||
icon: LayoutGrid,
|
||||
},
|
||||
{
|
||||
title: 'Projects',
|
||||
href: projectsIndex(),
|
||||
icon: Folder,
|
||||
},
|
||||
];
|
||||
|
||||
const mainNavItems: NavItem[] = [
|
||||
{
|
||||
title: 'Dashboard',
|
||||
href: dashboard(),
|
||||
icon: LayoutGrid,
|
||||
},
|
||||
];
|
||||
if (page.props.auth.permissions.projectCreate) {
|
||||
items.push({
|
||||
title: 'Project templates',
|
||||
href: projectTemplatesIndex(),
|
||||
icon: Layers3,
|
||||
});
|
||||
}
|
||||
|
||||
const rightNavItems: NavItem[] = [
|
||||
{
|
||||
title: 'Repository',
|
||||
href: 'https://github.com/laravel/vue-starter-kit',
|
||||
icon: Folder,
|
||||
},
|
||||
{
|
||||
title: 'Documentation',
|
||||
href: 'https://laravel.com/docs/starter-kits#vue',
|
||||
icon: BookOpen,
|
||||
},
|
||||
];
|
||||
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>
|
||||
<div>
|
||||
<div class="border-sidebar-border/80 border-b">
|
||||
<div class="mx-auto flex h-16 items-center px-4 md:max-w-7xl">
|
||||
<!-- Mobile Menu -->
|
||||
<div class="lg:hidden">
|
||||
<Sheet>
|
||||
<SheetTrigger :as-child="true">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="mr-2 h-9 w-9"
|
||||
>
|
||||
<Menu class="h-5 w-5" />
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="left" class="w-[300px] p-6">
|
||||
<SheetTitle class="sr-only"
|
||||
>Navigation menu</SheetTitle
|
||||
>
|
||||
<SheetHeader class="flex justify-start text-left">
|
||||
<AppLogoIcon
|
||||
class="size-6 fill-current text-black dark:text-white"
|
||||
/>
|
||||
</SheetHeader>
|
||||
<div
|
||||
class="flex h-full flex-1 flex-col justify-between space-y-4 py-6"
|
||||
>
|
||||
<nav class="-mx-3 space-y-1">
|
||||
<Link
|
||||
v-for="item in mainNavItems"
|
||||
:key="item.title"
|
||||
:href="item.href"
|
||||
class="hover:bg-accent flex items-center gap-x-3 rounded-lg px-3 py-2 text-sm font-medium"
|
||||
:class="
|
||||
whenCurrentUrl(
|
||||
item.href,
|
||||
activeItemStyles,
|
||||
)
|
||||
"
|
||||
>
|
||||
<component
|
||||
v-if="item.icon"
|
||||
:is="item.icon"
|
||||
class="h-5 w-5"
|
||||
/>
|
||||
{{ item.title }}
|
||||
</Link>
|
||||
</nav>
|
||||
<div class="flex flex-col space-y-4">
|
||||
<a
|
||||
v-for="item in rightNavItems"
|
||||
:key="item.title"
|
||||
:href="toUrl(item.href)"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="flex items-center space-x-2 text-sm font-medium"
|
||||
>
|
||||
<component
|
||||
v-if="item.icon"
|
||||
:is="item.icon"
|
||||
class="h-5 w-5"
|
||||
/>
|
||||
<span>{{ item.title }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
|
||||
<Link :href="dashboard()" class="flex items-center gap-x-2">
|
||||
<AppLogo />
|
||||
</Link>
|
||||
|
||||
<!-- Desktop Menu -->
|
||||
<div class="hidden h-full lg:flex lg:flex-1">
|
||||
<NavigationMenu class="ml-10 flex h-full items-stretch">
|
||||
<NavigationMenuList
|
||||
class="flex h-full items-stretch space-x-2"
|
||||
<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"
|
||||
>
|
||||
<NavigationMenuItem
|
||||
v-for="(item, index) in mainNavItems"
|
||||
:key="index"
|
||||
class="relative flex h-full items-center"
|
||||
<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)"
|
||||
>
|
||||
<Link
|
||||
:class="[
|
||||
navigationMenuTriggerStyle(),
|
||||
whenCurrentUrl(
|
||||
item.href,
|
||||
activeItemStyles,
|
||||
),
|
||||
'h-9 cursor-pointer px-3',
|
||||
]"
|
||||
:href="item.href"
|
||||
>
|
||||
<component
|
||||
v-if="item.icon"
|
||||
:is="item.icon"
|
||||
class="mr-2 h-4 w-4"
|
||||
/>
|
||||
{{ item.title }}
|
||||
</Link>
|
||||
<div
|
||||
v-if="isCurrentUrl(item.href)"
|
||||
class="absolute bottom-0 left-0 h-0.5 w-full translate-y-px bg-black dark:bg-white"
|
||||
></div>
|
||||
</NavigationMenuItem>
|
||||
</NavigationMenuList>
|
||||
</NavigationMenu>
|
||||
<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>
|
||||
|
||||
<div class="ml-auto flex items-center space-x-2">
|
||||
<div class="relative flex items-center space-x-1">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger :as-child="true">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="group h-9 w-9 cursor-pointer"
|
||||
class="focus-visible:ring-primary/30 hover:bg-surface-container-high size-10 rounded-full p-1 focus-visible:ring-2"
|
||||
>
|
||||
<Search
|
||||
class="size-5 opacity-80 group-hover:opacity-100"
|
||||
/>
|
||||
<UserAvatar rounded />
|
||||
<span class="sr-only">Open user menu</span>
|
||||
</Button>
|
||||
|
||||
<div class="hidden space-x-1 lg:flex">
|
||||
<template
|
||||
v-for="item in rightNavItems"
|
||||
:key="item.title"
|
||||
>
|
||||
<TooltipProvider :delay-duration="0">
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
as-child
|
||||
class="group h-9 w-9 cursor-pointer"
|
||||
>
|
||||
<a
|
||||
:href="toUrl(item.href)"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span class="sr-only">{{
|
||||
item.title
|
||||
}}</span>
|
||||
<component
|
||||
:is="item.icon"
|
||||
class="size-5 opacity-80 group-hover:opacity-100"
|
||||
/>
|
||||
</a>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{{ item.title }}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger :as-child="true">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="focus-within:ring-primary relative size-10 w-auto rounded-full p-1 focus-within:ring-2"
|
||||
>
|
||||
<UserAvatar rounded />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" class="w-56">
|
||||
<UserMenuContent :user="auth.user" />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</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-sidebar-border/70 flex w-full border-b"
|
||||
class="border-outline-variant/70 bg-surface-container-low/70 border-t"
|
||||
>
|
||||
<div
|
||||
class="mx-auto flex h-12 w-full items-center justify-start px-4 text-neutral-500 md:max-w-7xl"
|
||||
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>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
@@ -4,13 +4,13 @@ import AppLogoIcon from '@/components/AppLogoIcon.vue';
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-md"
|
||||
class="bg-primary text-primary-foreground flex aspect-square size-9 items-center justify-center rounded-lg shadow-[0_8px_24px_rgba(0,110,47,0.18)]"
|
||||
>
|
||||
<AppLogoIcon class="size-5 fill-current text-white dark:text-black" />
|
||||
<AppLogoIcon class="size-5 fill-current" />
|
||||
</div>
|
||||
<div class="ml-1 grid flex-1 text-left text-sm">
|
||||
<span class="mb-0.5 truncate font-semibold leading-tight"
|
||||
>Laravel Starter Kit</span
|
||||
>
|
||||
<span class="text-primary truncate text-sm font-semibold leading-tight">
|
||||
KanbanGit
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -15,15 +15,15 @@ defineProps<Props>();
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 40 42"
|
||||
viewBox="0 0 32 32"
|
||||
:class="className"
|
||||
v-bind="$attrs"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M17.2 5.633 8.6.855 0 5.633v26.51l16.2 9 16.2-9v-8.442l7.6-4.223V9.856l-8.6-4.777-8.6 4.777V18.3l-5.6 3.111V5.633ZM38 18.301l-5.6 3.11v-6.157l5.6-3.11V18.3Zm-1.06-7.856-5.54 3.078-5.54-3.079 5.54-3.078 5.54 3.079ZM24.8 18.3v-6.157l5.6 3.111v6.158L24.8 18.3Zm-1 1.732 5.54 3.078-13.14 7.302-5.54-3.078 13.14-7.3v-.002Zm-16.2 7.89 7.6 4.222V38.3L2 30.966V7.92l5.6 3.111v16.892ZM8.6 9.3 3.06 6.222 8.6 3.143l5.54 3.08L8.6 9.3Zm21.8 15.51-13.2 7.334V38.3l13.2-7.334v-6.156ZM9.6 11.034l5.6-3.11v14.6l-5.6 3.11v-14.6Z"
|
||||
/>
|
||||
<rect x="4" y="5" width="7" height="22" rx="2" />
|
||||
<rect x="13" y="5" width="7" height="14" rx="2" opacity="0.82" />
|
||||
<rect x="22" y="5" width="6" height="18" rx="2" opacity="0.64" />
|
||||
<path d="M14 24h7v3h-7z" opacity="0.82" />
|
||||
<path d="M22 26h6v1h-6z" opacity="0.64" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
@@ -15,7 +15,10 @@ const isOpen = usePage().props.sidebarOpen;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="variant === 'header'" class="flex min-h-screen w-full flex-col">
|
||||
<div
|
||||
v-if="variant === 'header'"
|
||||
class="bg-background text-foreground flex min-h-screen w-full flex-col"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
<SidebarProvider v-else :default-open="isOpen">
|
||||
|
||||
@@ -76,12 +76,12 @@ const mainNavItems = computed<NavItem[]>(() => {
|
||||
const footerNavItems: NavItem[] = [
|
||||
{
|
||||
title: 'Repository',
|
||||
href: 'https://github.com/laravel/vue-starter-kit',
|
||||
href: 'https://git.twyco.de/OpenSource/KanbanGit',
|
||||
icon: FolderGit2,
|
||||
},
|
||||
{
|
||||
title: 'Documentation',
|
||||
href: 'https://laravel.com/docs/starter-kits#vue',
|
||||
href: 'https://git.twyco.de/OpenSource/KanbanGit/src/branch/main/README.md',
|
||||
icon: BookOpen,
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user