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:
2026-05-12 00:46:03 +02:00
parent f8fddc0e14
commit 3df55b4d1c

View File

@@ -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>