Bespoke navigation system shared across all WLTH products. Composed of a product switcher, nav pill, entity selector, and utility cluster — all driven by the useHeader composable. Each consuming app configures its product variant once via app.config.ts.
Live render with mock data. Switch the product variant below to see how the header adapts.
How to add the header to a consuming app and configure the product variant.
Step 1 — Extend the design system layer
In the consuming app's nuxt.config.ts, extend the layer:
// nuxt.config.ts
export default defineNuxtConfig({
extends: ['@wlth/design-system']
})All header components and composables are auto-imported — no explicit imports needed.
Step 2 — Configure the product variant
Set wlth.product to the product ID. Provide wlth.navItems with real routes for this app — these override the design system defaults.
// app/app.config.ts
export default defineAppConfig({
wlth: {
product: 'pay',
navItems: [
{ label: 'Home', isHome: true, to: '/' },
{ label: 'Payments', icon: 'i-lucide-credit-card', to: '/payments' },
{ label: 'Payees', icon: 'i-lucide-users', to: '/payees' },
{ label: 'Cards', icon: 'i-lucide-wallet', to: '/cards' }
]
}
}) If navItems is omitted, the header uses the defaults from useHeader.ts. If product is omitted, it defaults to broker.
Step 3 — Place the component
Drop <AppHeader /> into your app layout:
// layouts/default.vue
<template>
<div class="flex flex-col min-h-screen">
<AppHeader />
<main class="flex-1">
<slot />
</main>
</div>
</template>Available product IDs
Broker
brokerHome, Products, Applications, Support
Pay
payHome, Payments, Payees, Cards
Shareholder
shareholderHome, Documents, Transactions
Dashboard
dashboardHome
Customer
customerHome
ProductNavItem fields
interface ProductNavItem {
label: string // Display text
icon?: string // Lucide icon (e.g. 'i-lucide-credit-card')
isHome?: boolean // Renders as icon-only house button
isCrossProduct?: boolean // Opens cross-product switch in a new tab
hasDropdown?: boolean // Shows a chevron indicator
to?: string // NuxtLink route — in-app navigation
href?: string // Anchor href — cross-app / external navigation
}The header is composed of smaller components and composables — all auto-imported by the layer. You can use them individually if needed.
Components
AppHeader
app/components/AppHeader.vueRoot header shell. Three-zone layout: left (logo + product switcher), center (nav pill), right (utility cluster). Height adapts from 64px mobile → 80px tablet → 100px desktop.
ProductSwitcher
app/components/ProductSwitcher.vuePill button showing the active product label. Opens a dropdown to switch between all products — each opening in a new tab at the product's base URL.
ProductNav
app/components/ProductNav.vueHorizontal pill nav bar (tablet+ only) rendering the active product's navItems. Supports to/href routing on each item. Tracks active label via useHeader.
EntitySelector
app/components/EntitySelector.vueDisplays the current entity avatar and name. Opens a dropdown to switch entities. Persists selection to localStorage via useHeader.
HeaderUtilityCluster
app/components/HeaderUtilityCluster.vueRight-side pill grouping entity selector, notifications tray, and user avatar. Composed from EntitySelector, NotificationsTray, and ProfileMenu.
ProfileMenu
app/components/ProfileMenu.vueUser avatar button with dropdown showing name, email, role switcher, and sign out. Reads from MOCK_USER in useHeader.
NotificationsTray
app/components/NotificationsTray.vueBell icon with unread count badge. Opens NotificationsSlideover. Reads filtered notifications from useHeader.
NotificationsSlideover
app/components/NotificationsSlideover.vueSlideover listing all notifications with read/unread state, mark-all-read, and scope toggle (all products vs current product).
MobileMenuDrawer
app/components/MobileMenuDrawer.vueFull-height drawer for mobile (<md). Shows product switcher, nav items, entity selector, and profile — everything hidden behind the hamburger menu.
AppPageLoader
app/components/AppPageLoader.vueThin progress bar at the top of the page driven by usePageLoader composable. Triggered on route navigation start/end.
Composables
useHeader
app/composables/useHeader.tsShared composable (createSharedComposable) managing current product, current entity, notifications, roles, and actions. Reads initial product from app.config wlth.product. Persists entity and scope to localStorage via VueUse.
useDashboard
app/composables/useDashboard.tsManages mobile menu open state and sidebar collapsed state. Used by AppHeader to trigger the MobileMenuDrawer.
usePageLoader
app/composables/usePageLoader.tsControls the AppPageLoader progress bar. Call start() on navigation begin and finish() on navigation end.
Call useHeader() anywhere in a consuming app to read or modify header state. It is a shared composable — every call returns the same instance.
const {
currentProductId, // Ref<ProductId>
currentProduct, // ComputedRef<Product>
currentEntityId, // Ref<string>
currentEntity, // ComputedRef<EntityOption>
notifications, // Ref<HubNotification[]>
filteredNotifications, // ComputedRef<HubNotification[]> — filtered by notificationScope
unreadCount, // ComputedRef<number>
notificationScope, // Ref<'all' | 'product'> — persisted to localStorage
roles, // Ref<string[]> — persisted to localStorage
activeLabel, // Ref<string> — currently active nav item label
switchProduct, // (id: ProductId) => void — opens product in new tab
setEntity, // (id: string) => void — updates currentEntityId
markRead, // (id: string) => void — marks one notification as read
markAllRead, // () => void — marks all filtered notifications as read
} = useHeader()currentProductIdRef<ProductId>The active product. Initialised from app.config wlth.product, writable — changing it switches the header context.currentProductComputedRef<Product>Full product object for the active product, with navItems merged from app.config.currentEntityIdRef<string>ID of the active entity. Persisted to localStorage under wlth-entity-id.currentEntityComputedRef<EntityOption>Full EntityOption for the active entity, resolved from the entities list.notificationsRef<HubNotification[]>The full notification list. Replace this ref's value to load real notifications from your API.filteredNotificationsComputedRef<HubNotification[]>Notifications filtered by notificationScope (all products, or current product only).unreadCountComputedRef<number>Count of unread notifications in filteredNotifications. Drives the badge on the bell icon.notificationScopeRef<'all' | 'product'>Controls whether filteredNotifications shows all products or just the current product.rolesRef<string[]>Active user roles. Persisted to localStorage under wlth-roles. Use to gate features in consuming apps.activeLabelRef<string>Label of the currently active nav item. Used by ProductNav to highlight the active pill. Resets to "Home" on product switch.switchProduct(id: ProductId) => voidOpens the target product in a new tab, passing the current entityId as a query param for context handoff.setEntity(id: string) => voidUpdates the active entity and persists to localStorage.markRead(id: string) => voidMarks a single notification as read by its id.markAllRead() => voidMarks all notifications in the current filtered scope as read.Controls mobile menu and notifications slideover state. Also registers app-wide keyboard shortcuts. It is a shared composable — every call returns the same instance.
const {
isMobileMenuOpen, // Ref<boolean> — drives MobileMenuDrawer
isNotificationsSlideoverOpen, // Ref<boolean> — drives NotificationsSlideover
} = useDashboard()Keyboard shortcuts
Registered automatically when useDashboard() is first called. These are global shortcuts — they fire on any page.
The routes above are defaults. Override them in a consuming app by calling defineShortcuts() after useDashboard() is initialised.
Drives the thin progress bar at the top of the page rendered by AppPageLoader. Call show() to start it — it disappears automatically after the duration you provide.
const { isVisible, show } = usePageLoader()
// show() accepts an optional minimum display time in milliseconds (default 2200ms)
show() // visible for 2.2 seconds
show(1000) // visible for 1 secondWiring to Nuxt route changes
The recommended approach is a Nuxt plugin that calls show() on each route change. Create this file in your consuming app:
// plugins/page-loader.client.ts
export default defineNuxtPlugin(() => {
const { show } = usePageLoader()
const router = useRouter()
router.beforeEach(() => {
show(600) // show for 600ms — tune to match your typical page load time
})
})The loader also works well for async data fetching — call show() before an await and it disappears on its own without needing a finish() call.
The design system ships with mock entities, a mock user, and mock notifications so the header renders out of the box. In a real app you replace these by writing to the refs that useHeader() exposes. The recommended place to do this is a Nuxt plugin that runs after your auth session is established.
Replace notifications
Assign your API response directly to notifications.value. The header will reactively update.
// plugins/header-data.client.ts
export default defineNuxtPlugin(async () => {
const { notifications } = useHeader()
// Replace with your API call
const data = await $fetch('/api/notifications')
notifications.value = data.map(n => ({
id: n.id,
product: n.product, // must be a valid ProductId
entityId: n.entityId,
title: n.title,
body: n.body, // optional
timestamp: n.createdAt, // ISO string
read: n.isRead,
}))
})Replace entities and user
MOCK_ENTITIES and MOCK_USER are exported constants from useHeader.ts. Because the entity list and user profile are currently hardcoded in the composable, swapping them requires either overriding those exports or — the cleaner approach — contributing a setEntities() and setUser() method to the composable. This is a known gap and the recommended next step for apps that need dynamic entity lists.
HubNotification shape
interface HubNotification {
id: string
product: ProductId // 'broker' | 'pay' | 'shareholder' | 'dashboard' | 'customer'
entityId: string // must match an entity id
title: string
body?: string // optional subtitle
timestamp: string // ISO 8601 date string
read: boolean
}Changes to this design system do not automatically propagate to consuming apps. Apps pin to a specific git tag and must opt in to updates.
Release workflow
version in package.jsongit tag vX.Y.Z && git push origin vX.Y.Znpm install// consuming app — package.json
"@wlth/design-system": "github:rhyeezus/wlth-design-system#v1.1.0" During active development, a consuming app can temporarily pin to #main to get changes without releasing — but this is unstable for production.