Redesign app layout chrome
linter / quality (pull_request) Successful in 2m0s
tests / ci (8.4) (pull_request) Successful in 1m57s

This commit is contained in:
2026-06-25 01:56:54 +02:00
parent 0f125f17a3
commit 59817ad30f
8 changed files with 477 additions and 312 deletions
+1 -1
View File
@@ -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 />
+187 -217
View File
@@ -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>
+5 -5
View File
@@ -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>
+7 -7
View File
@@ -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>
+4 -1
View File
@@ -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">
+2 -2
View File
@@ -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,
},
];