Section headings ("Beheer" / organisation name, "Platform") were
already uppercase + muted but read as bold paragraph dividers more
than as quiet group markers. Tighten letter-spacing, drop weight
from semibold to medium, lighten the color one step (surface-500 →
surface-400), and shrink text to 11px so the headings recede and
let the nav items themselves carry the visual weight.
Spacing nudged from mt-4/mb-2/px-2 → mt-6/mb-1/px-3: more breathing
room above each group, less below (the items already have py-2 on
top), and the heading left-edge now lines up with the icons of the
nav items beneath it (both at px-3).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
213 lines
5.9 KiB
Vue
213 lines
5.9 KiB
Vue
<script setup lang="ts">
|
|
// AppShell — PrimeVue-only application chrome introduced in F3 per the
|
|
// RFC-WS-FRONTEND-PRIMEVUE AD-3 layout rewrite, B7-option-B (alongside
|
|
// the Vuexy carrier DefaultLayoutWithVerticalNav.vue).
|
|
//
|
|
// Hard constraint per F3 sprint plan: no Vuetify or @core/@layouts
|
|
// imports inside this component. Vuetify-based features that previously
|
|
// lived in the topbar (search, theme switcher, notifications,
|
|
// org switcher, context switcher, shortcuts, impersonation banner,
|
|
// rich user profile menu) are absent from AppShell — they migrate in
|
|
// F4 sub-packages and re-enter through this component then. See the
|
|
// B7 commit body for the explicit regression list.
|
|
//
|
|
// Layout: Tailwind CSS grid. Desktop (lg+) shows a permanent sidebar;
|
|
// mobile (<lg) hides it and shows a hamburger toggle that opens a
|
|
// PrimeVue Drawer overlay. Content area renders the default slot
|
|
// (a RouterView from the wrapping layout file).
|
|
|
|
import { computed, ref } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import Drawer from 'primevue/drawer'
|
|
import Button from 'primevue/button'
|
|
import Avatar from 'primevue/avatar'
|
|
import Menu from 'primevue/menu'
|
|
import Icon from '@/components/Icon.vue'
|
|
import { useAuthStore } from '@/stores/useAuthStore'
|
|
|
|
interface NavHeading {
|
|
heading: string
|
|
}
|
|
interface NavLink {
|
|
title: string
|
|
to: { name: string }
|
|
icon: { icon: string }
|
|
}
|
|
type NavItem = NavHeading | NavLink
|
|
|
|
interface Props {
|
|
navItems: NavItem[]
|
|
title?: string
|
|
}
|
|
|
|
withDefaults(defineProps<Props>(), {
|
|
title: 'Crewli',
|
|
})
|
|
|
|
const router = useRouter()
|
|
const authStore = useAuthStore()
|
|
|
|
const mobileNavOpen = ref(false)
|
|
const userMenuRef = ref<InstanceType<typeof Menu> | null>(null)
|
|
|
|
const userInitial = computed(() => {
|
|
const name = authStore.user?.full_name ?? ''
|
|
|
|
return name.charAt(0).toUpperCase() || '?'
|
|
})
|
|
|
|
const userMenuItems = computed(() => [
|
|
{
|
|
label: authStore.user?.full_name ?? 'Gebruiker',
|
|
items: [
|
|
{
|
|
label: 'Mijn Profiel',
|
|
icon: 'tabler-user',
|
|
command: () => router.push({ name: 'account-settings' }),
|
|
},
|
|
{
|
|
label: 'Uitloggen',
|
|
icon: 'tabler-logout',
|
|
command: async () => {
|
|
await authStore.logout()
|
|
await router.push('/login')
|
|
},
|
|
},
|
|
],
|
|
},
|
|
])
|
|
|
|
function isHeading(item: NavItem): item is NavHeading {
|
|
return 'heading' in item
|
|
}
|
|
|
|
function navigate(item: NavLink) {
|
|
mobileNavOpen.value = false
|
|
router.push(item.to)
|
|
}
|
|
|
|
function toggleUserMenu(event: Event) {
|
|
userMenuRef.value?.toggle(event)
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="crewli-app-shell flex min-h-screen">
|
|
<!-- Desktop sidebar (lg+) -->
|
|
<aside class="hidden lg:flex w-72 flex-col border-r border-surface-200 bg-surface-0">
|
|
<SidebarHeader :title="title" />
|
|
<nav class="flex-1 overflow-y-auto p-3">
|
|
<template
|
|
v-for="(item, idx) in navItems"
|
|
:key="idx"
|
|
>
|
|
<div
|
|
v-if="isHeading(item)"
|
|
class="mt-6 mb-1 px-3 text-[11px] font-medium uppercase tracking-widest text-surface-400"
|
|
>
|
|
{{ item.heading }}
|
|
</div>
|
|
<button
|
|
v-else
|
|
type="button"
|
|
class="flex w-full items-center gap-3 rounded px-3 py-2 text-sm text-surface-700 transition hover:bg-primary-50 hover:text-primary-600"
|
|
@click="navigate(item)"
|
|
>
|
|
<Icon
|
|
:name="item.icon.icon"
|
|
size="20"
|
|
/>
|
|
<span>{{ item.title }}</span>
|
|
</button>
|
|
</template>
|
|
</nav>
|
|
</aside>
|
|
|
|
<!-- Mobile drawer (overlay, <lg) -->
|
|
<Drawer
|
|
v-model:visible="mobileNavOpen"
|
|
position="left"
|
|
class="lg:hidden"
|
|
:pt="{ root: { style: { width: '18rem' } }, header: { class: 'p-0' } }"
|
|
>
|
|
<template #header>
|
|
<SidebarHeader :title="title" />
|
|
</template>
|
|
<nav class="flex flex-col p-3">
|
|
<template
|
|
v-for="(item, idx) in navItems"
|
|
:key="idx"
|
|
>
|
|
<div
|
|
v-if="isHeading(item)"
|
|
class="mt-6 mb-1 px-3 text-[11px] font-medium uppercase tracking-widest text-surface-400"
|
|
>
|
|
{{ item.heading }}
|
|
</div>
|
|
<button
|
|
v-else
|
|
type="button"
|
|
class="flex w-full items-center gap-3 rounded px-3 py-2 text-sm text-surface-700 transition hover:bg-primary-50 hover:text-primary-600"
|
|
@click="navigate(item)"
|
|
>
|
|
<Icon
|
|
:name="item.icon.icon"
|
|
size="20"
|
|
/>
|
|
<span>{{ item.title }}</span>
|
|
</button>
|
|
</template>
|
|
</nav>
|
|
</Drawer>
|
|
|
|
<!-- Main column -->
|
|
<div class="flex flex-1 flex-col min-w-0">
|
|
<!-- Top bar -->
|
|
<header class="flex h-16 items-center justify-between border-b border-surface-200 bg-surface-0 px-4">
|
|
<div class="flex items-center gap-2">
|
|
<Button
|
|
class="lg:hidden"
|
|
severity="secondary"
|
|
text
|
|
rounded
|
|
aria-label="Menu openen"
|
|
@click="mobileNavOpen = true"
|
|
>
|
|
<Icon
|
|
name="tabler-menu-2"
|
|
size="24"
|
|
/>
|
|
</Button>
|
|
<span class="text-base font-medium text-surface-700 lg:hidden">{{ title }}</span>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-2">
|
|
<Button
|
|
severity="secondary"
|
|
text
|
|
rounded
|
|
aria-label="Gebruikersmenu openen"
|
|
@click="toggleUserMenu"
|
|
>
|
|
<Avatar
|
|
:label="userInitial"
|
|
shape="circle"
|
|
class="bg-primary-500 text-white"
|
|
/>
|
|
</Button>
|
|
<Menu
|
|
ref="userMenuRef"
|
|
:model="userMenuItems"
|
|
:popup="true"
|
|
/>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Page content -->
|
|
<main class="flex-1 overflow-x-hidden p-4 lg:p-6">
|
|
<slot />
|
|
</main>
|
|
</div>
|
|
</div>
|
|
</template>
|