feat(appshell): topbar breadcrumb, notification bell, and help icon
Left side gains a desktop-only breadcrumb "Organisation / Page title" using the current organisation from useAuthStore and a page title resolved by: 1. route.meta.title (if a page sets it explicitly), then 2. matching the active route name against the navItems prop, then 3. humanizing the route name as a last-resort fallback. The chevron separator is suppressed when either side is empty, so portal and pre-org users see just the page title. Mobile preserves the existing hamburger + title text (the breadcrumb is hidden on <lg to keep the topbar single-row). Right side gains a notification bell and a help icon. The bell is a visual placeholder (no badge) — clicking shows a PrimeVue Toast "Notificaties komen binnenkort beschikbaar" until the notification framework lands as a separate sprint. The help icon would normally open https://docs.crewli.app in a new tab, but the host currently serves with a TLS cert that does not cover the name (ERR_TLS_CERT_ALTNAME_INVALID), so the click handler falls back to a Toast. A TODO comment in the source records the target URL and the one-line switch to make once the cert is fixed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,11 +16,13 @@
|
||||
// PrimeVue Drawer overlay. Content area renders the default slot
|
||||
// (a RouterView from the wrapping layout file).
|
||||
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import Drawer from 'primevue/drawer'
|
||||
import Button from 'primevue/button'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import Icon from '@/components/Icon.vue'
|
||||
import { useAuthStore } from '@/stores/useAuthStore'
|
||||
|
||||
interface NavHeading {
|
||||
heading: string
|
||||
@@ -37,11 +39,14 @@ interface Props {
|
||||
title?: string
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
title: 'Crewli',
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const toast = useToast()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const mobileNavOpen = ref(false)
|
||||
|
||||
@@ -53,6 +58,54 @@ function navigate(item: NavLink) {
|
||||
mobileNavOpen.value = false
|
||||
router.push(item.to)
|
||||
}
|
||||
|
||||
// Breadcrumb: "Organisation / Page title". Page title resolves from
|
||||
// route.meta.title → matching navItems entry → humanized route name.
|
||||
// Org name is omitted on portal / pre-org users (currentOrganisation
|
||||
// is null) so the breadcrumb reads as just the page title there.
|
||||
const orgName = computed(() => authStore.currentOrganisation?.name ?? '')
|
||||
|
||||
const pageTitle = computed(() => {
|
||||
const metaTitle = route.meta.title
|
||||
if (typeof metaTitle === 'string' && metaTitle.length > 0)
|
||||
return metaTitle
|
||||
|
||||
const match = props.navItems.find(
|
||||
(item): item is NavLink => 'to' in item && item.to.name === route.name,
|
||||
)
|
||||
|
||||
if (match)
|
||||
return match.title
|
||||
|
||||
const raw = String(route.name ?? '')
|
||||
if (!raw)
|
||||
return ''
|
||||
|
||||
return raw.charAt(0).toUpperCase() + raw.slice(1).replace(/-/g, ' ')
|
||||
})
|
||||
|
||||
function onNotificationsClick() {
|
||||
toast.add({
|
||||
severity: 'info',
|
||||
summary: 'Notificaties',
|
||||
detail: 'Notificaties komen binnenkort beschikbaar.',
|
||||
life: 3000,
|
||||
})
|
||||
}
|
||||
|
||||
// TODO(docs-url): https://docs.crewli.app currently serves with a TLS
|
||||
// cert that does not cover the host (ERR_TLS_CERT_ALTNAME_INVALID),
|
||||
// so a browser hit shows a security warning instead of the docs.
|
||||
// When the cert is fixed, switch this handler to:
|
||||
// window.open('https://docs.crewli.app', '_blank', 'noopener,noreferrer')
|
||||
function onHelpClick() {
|
||||
toast.add({
|
||||
severity: 'info',
|
||||
summary: 'Help',
|
||||
detail: 'Documentatie komt binnenkort beschikbaar.',
|
||||
life: 3000,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -149,6 +202,49 @@ function navigate(item: NavLink) {
|
||||
/>
|
||||
</Button>
|
||||
<span class="text-base font-medium text-surface-700 lg:hidden">{{ title }}</span>
|
||||
<nav
|
||||
class="hidden items-center gap-2 text-sm lg:flex"
|
||||
aria-label="Kruimelpad"
|
||||
>
|
||||
<span
|
||||
v-if="orgName"
|
||||
class="text-surface-500"
|
||||
>{{ orgName }}</span>
|
||||
<Icon
|
||||
v-if="orgName && pageTitle"
|
||||
name="tabler-chevron-right"
|
||||
size="14"
|
||||
class="text-surface-400"
|
||||
/>
|
||||
<span class="font-medium text-surface-900">{{ pageTitle }}</span>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1">
|
||||
<Button
|
||||
severity="secondary"
|
||||
text
|
||||
rounded
|
||||
aria-label="Notificaties"
|
||||
@click="onNotificationsClick"
|
||||
>
|
||||
<Icon
|
||||
name="tabler-bell"
|
||||
size="22"
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
severity="secondary"
|
||||
text
|
||||
rounded
|
||||
aria-label="Help"
|
||||
@click="onHelpClick"
|
||||
>
|
||||
<Icon
|
||||
name="tabler-help"
|
||||
size="22"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user