App Header

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.

WLTH Hub — Live Demo (pencil-dev-test.vercel.app — rename pending)

Preview

Live render with mock data. Switch the product variant below to see how the header adapts.

Product variant

Usage

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

broker

Home, Products, Applications, Support

Pay

pay

Home, Payments, Payees, Cards

Shareholder

shareholder

Home, Documents, Transactions

Dashboard

dashboard

Home

Customer

customer

Home

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
}

Architecture

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.vue

Root 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.vue

Pill 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.vue

Horizontal 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.vue

Displays the current entity avatar and name. Opens a dropdown to switch entities. Persists selection to localStorage via useHeader.

HeaderUtilityCluster

app/components/HeaderUtilityCluster.vue

Right-side pill grouping entity selector, notifications tray, and user avatar. Composed from EntitySelector, NotificationsTray, and ProfileMenu.

ProfileMenu

app/components/ProfileMenu.vue

User avatar button with dropdown showing name, email, role switcher, and sign out. Reads from MOCK_USER in useHeader.

NotificationsTray

app/components/NotificationsTray.vue

Bell icon with unread count badge. Opens NotificationsSlideover. Reads filtered notifications from useHeader.

NotificationsSlideover

app/components/NotificationsSlideover.vue

Slideover listing all notifications with read/unread state, mark-all-read, and scope toggle (all products vs current product).

MobileMenuDrawer

app/components/MobileMenuDrawer.vue

Full-height drawer for mobile (<md). Shows product switcher, nav items, entity selector, and profile — everything hidden behind the hamburger menu.

AppPageLoader

app/components/AppPageLoader.vue

Thin progress bar at the top of the page driven by usePageLoader composable. Triggered on route navigation start/end.

Composables

useHeader

app/composables/useHeader.ts

Shared 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.ts

Manages mobile menu open state and sidebar collapsed state. Used by AppHeader to trigger the MobileMenuDrawer.

usePageLoader

app/composables/usePageLoader.ts

Controls the AppPageLoader progress bar. Call start() on navigation begin and finish() on navigation end.

useHeader API

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()
NameTypeDescription
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.

useDashboard API

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.

G then HNavigate to / (home)
G then INavigate to /inbox
G then CNavigate to /customers
G then SNavigate to /settings
G then DNavigate to /design-system
NToggle notifications slideover

The routes above are defaults. Override them in a consuming app by calling defineShortcuts() after useDashboard() is initialised.

usePageLoader API

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 second

Wiring 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.

Connecting real data

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.

EntityOption shape
Each entity needs: id (string), name (string), and optionally avatar (Nuxt UI AvatarProps — src + alt). If no src is provided, UAvatar renders initials from alt.

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
}

Versioning & updates

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

  1. 1 Make and test changes in this repo
  2. 2 Bump version in package.json
  3. 3Run git tag vX.Y.Z && git push origin vX.Y.Z
  4. 4In each consuming app: update the pinned tag and run npm 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.