Header
The single h-16 top bar of the Luminaux shell — an optional mega-nav on the left and a right-cluster of icon controls (search, design-system, apps, language, fullscreen, settings, theme, notifications, profile).
Overview
The header is a single h-16 row rendered by src/layout/Header.tsx. It is mounted once by
AppLayout and stays fixed above the routed content. It has two regions:
- Left — the mobile menu button (drawer toggle,
lg:hidden), an optional brand lockup (only in header-only mode), and the primary navigationHeaderMegaNav(gated byconfig.showPrimaryNav,lg:flex). This is not the sidebar tree — it is a curated set of showcase mega-menus. - Right cluster (
ml-auto,shrink-0) — a fixed order of controls:search (⌘K) · design-system · apps · language · fullscreen · settings · theme · bell · profile.
The whole bar can auto-hide on scroll-down / reveal on scroll-up when config.autoHideNav is on
(useScroll + useMotionValueEvent), and the right cluster animates in with a staggered entrance
(headerStagger / headerStaggerItem from src/lib/motion.ts).
The header does not own any global state of its own — it is a presentational bar wired to shared stores
(useTheme, useLayout, useFullscreen, usePresence, useLanguage) and to the overlay open-state
that lives in AppLayout.
Architecture & files
| File | Responsibility |
|---|---|
src/layout/Header.tsx | The h-16 bar; left region + right cluster; auto-hide scroll logic. |
src/layout/AppLayout.tsx | Mounts the header, owns overlay open-state (searchOpen / customizerOpen / notificationsOpen), binds ⌘K, feeds useNotifications() into the header + panel, pushes visited routes via pushRoute. |
src/layout/mega/HeaderMegaNav.tsx | Desktop primary nav — one shared morphing dropdown across the four menu triggers. |
src/layout/mega/MegaPanels.tsx | The panel bodies: QuickAccessPanel, FlyoutMenuPanel, MegaMenuPanel, MultilevelPanel. |
src/layout/mega/MegaMobileNav.tsx | Mobile fallback — the showcase menus as accordions inside the sidebar drawer. |
src/data/megaMenu.ts | Data for the mega menus (quickAccess, resourcesMenu, megaTabs, footer links). |
src/layout/overlays/SearchOverlay.tsx | The ⌘K command palette. |
src/hooks/useRecentRoutes.ts | Recently-visited routes store feeding the palette's "Recent" group. |
src/layout/AppsMenu.tsx + src/data/apps.ts | The "waffle" app launcher grid. |
src/layout/DesignSystemMenu.tsx | The skin switcher with live per-skin colour swatches. |
src/layout/LanguageMenu.tsx + src/hooks/useLanguage.ts | The language dropdown. |
src/hooks/useFullscreen.ts | Browser fullscreen toggle. |
src/layout/ProfileMenu.tsx + src/data/user.ts + src/hooks/usePresence.ts | Avatar dropdown: identity, account links, presence switcher, sign-out. |
src/layout/overlays/NotificationsPanel.tsx + src/hooks/useNotifications.ts + src/data/notifications.ts | The notifications feed + right slide-in panel. |
src/lib/accent.ts | accentTile — categorical accent → literal token classes, used by AppsMenu. |
The right cluster (source order)
From Header.tsx, each control is wrapped in a motion.div variants={headerStaggerItem} for the cascade:
- Search — a ghost
Button(onSearchClick) with aSearchicon and a⌘K<kbd>chip (lg:inline). - DesignSystemMenu —
hidden sm:block. - AppsMenu —
hidden sm:block. - LanguageMenu — always shown.
- Fullscreen — icon
ButtoncallingtoggleFullscreen,hidden lg:block. - Settings — icon
Button(onCustomizerClick) withSettings2; opens the Layout Customizer. - Theme — icon
ButtoncallingtoggleTheme; theSun/Moonicon crossfades with anAnimatePresencerotate. - Bell — icon
Button(onNotificationsClick) with a pulsingunreadCountbadge (9+cap). - ProfileMenu — the avatar trigger + dropdown.
Usage
The header is mounted by AppLayout; you don't render it directly in pages. AppLayout passes the
callbacks and the unread count:
// src/layout/AppLayout.tsx (excerpt)
const {notifications, unreadCount, markRead, markAllRead, resolve} = useNotifications()
<Header
onMenuClick={() => setDrawerOpen(true)}
onCustomizerClick={() => setCustomizerOpen(true)}
onSearchClick={() => setSearchOpen(true)}
onNotificationsClick={() => setNotificationsOpen(true)}
unreadCount={unreadCount}
/>⌘K (or Ctrl-K) toggles the search overlay from anywhere, bound in AppLayout:
// Global ⌘K / Ctrl-K
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
e.preventDefault()
setSearchOpen((o) => !o)
}
}
window.addEventListener('keydown', onKey)
return () => window.removeEventListener('keydown', onKey)
}, [])API / Props
Header props (HeaderProps)
| Prop | Type | Description |
|---|---|---|
onMenuClick | () => void | Opens the mobile sidebar drawer. |
onCustomizerClick | () => void | Opens the Layout Customizer slide-in. |
onSearchClick | () => void | Opens the ⌘K search overlay. |
onNotificationsClick | () => void | Opens the notifications panel. |
unreadCount | number | Drives the bell badge (hidden when 0, capped at 9+). |
Controls at a glance
| Control | Component | What it does | Key hook/data |
|---|---|---|---|
| Mobile menu | inline <button> | Opens the sidebar drawer (lg:hidden). | onMenuClick |
| Primary nav | HeaderMegaNav | Four showcase mega-menus. Gated by config.showPrimaryNav (always on in header-only). | src/data/megaMenu.ts |
| Search | ghost Button → SearchOverlay | Command palette; also ⌘K. | useRecentRoutes, menu, useLayout, useTheme |
| Design system | DesignSystemMenu | Switches the visual skin live, with swatches + toast. | useLayout().setSkin, SKINS |
| Apps | AppsMenu | Waffle launcher grid; navigates or toasts a stub. | src/data/apps.ts, accentTile |
| Language | LanguageMenu | Picks the UI language; trigger shows the active flag. | useLanguage |
| Fullscreen | icon Button | Toggles browser fullscreen. | useFullscreen |
| Settings | icon Button | Opens the Layout Customizer. | onCustomizerClick |
| Theme | icon Button | Toggles light/dark. | useTheme().toggleTheme |
| Notifications | icon Button → NotificationsPanel | Feed with filter tabs + request approve/reject. | useNotifications |
| Profile | ProfileMenu | Identity, account links, presence, sign-out. | currentUser, usePresence |
Configuration & customization
Header presentation flags
The header reads useLayout().config and useRouteLayout():
config.showPrimaryNav— show/hide theHeaderMegaNavtriggers (still always shown when header-only).config.autoHideNav— hide on scroll-down, reveal on scroll-up. Mutually exclusive withstickyPageHeader(a hidden header would leave a gap above a stuck page header).config.fixedNav— keeps the header pinned to the top (structural, handled by the shell SCSS).config.headerOnlyoruseRouteLayout().forceHeaderOnly— hides the sidebar and surfaces the brand lockup in the header (fromsrc/config/brand.ts), and forces the mega-nav on. The/apps/chatroute forces header-only presentation via itsappRoutesentry.
See Customizer & Layout Settings for how these flags are toggled, and Layout & Shell for how the shell consumes them.
Mega navigation (HeaderMegaNav)
HeaderMegaNav is a single shared dropdown modelled on the Motion mega-menu example: one panel that
morphs size/position between triggers (a layout tween, fast rather than a spring to minimise text
distortion), with a sliding layoutId="mega-hl" highlight under the hovered/active trigger and a content
crossfade. It opens on hover and keyboard focus, closes on Escape and on mouse-leave (with a 140ms
grace timer). The trigger row is horizontally scrollable (scrollbar hidden) so the nav never has to
hide on narrow widths; the panel is a sibling of the scroll row so it isn't clipped.
The four menus come from MENUS in HeaderMegaNav.tsx, each rendering a panel from MegaPanels.tsx:
| Key | Label key (nav:) | Panel | Source data |
|---|---|---|---|
quick | mmQuickAccess | QuickAccessPanel | quickAccess (6 dashboards) + quickAccessFooter |
resources | mmResources | FlyoutMenuPanel | resourcesMenu (6 categories × 4 items) |
mega | mmMegaMenu | MegaMenuPanel (align: 'start') | megaTabs (UI / Pages / Components, 9 each) |
multilevel | multilevel | MultilevelPanel | mirrors the full sidebar menu |
Wide panels use align: 'start' to pin to the nav's left edge; the others clamp under their trigger
within an 8px viewport gutter. Item labels reuse the sidebar's nav: keys; descriptions/headings are
mega-specific nav: keys (mm-prefixed). See src/data/megaMenu.ts for the full tables and the
Accent type ('primary' | 'success' | 'warning' | 'danger' | 'info') used by card tiles.
Mobile fallback
On phones the hover panels don't fit, so MegaMobileNav renders Quick Access / Resources / Mega Menu as
vertical accordions inside the sidebar drawer (scope === 'mobile' in Sidebar.tsx). The Multilevel
menu is omitted there — it duplicates the sidebar tree already in the drawer.
⌘K command palette (SearchOverlay)
The palette builds its command set from the app's real data:
- Recent — recently-visited routes from
useRecentRoutes(auseSyncExternalStoremodule store, most-recent-first, de-duped, capped at 6, persisted tolocalStorage('recent-routes')).AppLayoutcallspushRoute(location.pathname)on every navigation. - Navigation — every leaf in
src/data/menu.ts(recursively walked) →navigate(to). - Appearance — a theme toggle plus one entry per skin in
SKINS(setSkin). - Layout — toggle minified sidebar / dual sidebar / fixed header (
update({...})).
It filters by label + keywords, groups in a fixed order (Recent → Navigation → Appearance → Layout),
supports full keyboard nav (ArrowUp/Down, Enter, Escape), a layoutId="cmd-active" sliding
highlight, and a Typewriter hint on the empty state. Text lives in the search namespace.
Apps launcher (AppsMenu)
A "waffle" grid of apps (src/data/apps.ts). Each tile's coloured icon uses accentTile[app.accent]
(src/lib/accent.ts) — literal token classes because Tailwind only emits class strings it sees
literally. Tiles with a to navigate; tiles without one fire an info toast (appStub). Labels resolve
in the header namespace. The panel pops via a shared gentle spring (MotionConfig) with a per-tile
scale-in stagger; dismissal uses useDismiss.
Design-system (skin) switcher (DesignSystemMenu)
A Palette-icon dropdown that swaps the visual skin live. Each option renders a real per-skin
swatch inside a data-skin={id} scope, so the swatch pulls that skin's actual --primary /
--success / --warning / --surface-muted tokens. Picking a skin calls setSkin(id) and fires a
success toast. See Design Skins for the skin mechanism.
Language switcher (LanguageMenu)
The trigger shows the active language flag; the dropdown is a two-column grid of flag + native name (+
English label when different). Selecting calls useLanguage().setLanguage(code), which persists via the
i18next detector. See Internationalization.
Fullscreen (useFullscreen)
Toggles document.documentElement.requestFullscreen() / exitFullscreen() and tracks state via the
fullscreenchange event (so Esc / F11 exits update the icon). Hidden below lg.
Profile menu (ProfileMenu)
The avatar trigger shows the user's avatar with a presence StatusDot, plus name + tagline
(t(currentUser.roleKey)) on sm:flex. The panel (origin-top-right popUp, sections cascade in) has:
- Identity — avatar, name, role, email (from
currentUserinsrc/data/user.ts). - Account links — Profile / Account Settings / Billing / Help (stubbed) + a Keyboard Shortcuts
row that dispatches the
open-shortcutswindow event. - Presence switcher —
online / away / busy / offlinevia the sharedusePresencestore (localStorage('user-status')), keeping the sidebar user-card in sync. - Sign out — removes
auth_token(API_CONFIG.tokenKey), toasts, and navigates to/auth/login.
Notifications panel (NotificationsPanel)
A right slide-in (same pattern as the customizer). useNotifications owns the feed + read state
(seeded from src/data/notifications.ts, text stored as i18n keys in the notifications namespace).
Features: filter tabs (all / unread / mentions), per-type icon + tint (typeMeta),
swipe-to-mark-read rows (SwipeRow), approve/reject actions on request-type rows (resolve), an
empty state, and a Mark all read footer. The bell badge (unreadCount) pulses when > 0.
Examples
Force header-only presentation on a route
Header-only is presentation-only — set the flag on the route's appRoutes entry (src/routes.tsx)
rather than mutating the persisted config:
// src/routes.tsx
{
path: '/apps/chat',
element: <ChatPage />,
lazy: false,
fullBleed: true,
headerOnly: true, // hides the sidebar; the header shows the brand + mega-nav
fixedFooter: true,
}Header.tsx reads this via useRouteLayout().forceHeaderOnly and mirrors the sidebar-less branches.
Add an app to the waffle launcher
// src/data/apps.ts
export const apps: AppLink[] = [
// ...
{key: 'appReports', to: '/reports', icon: FileBarChart, accent: 'info'},
]
// then add `header:appReports` to src/locales/en/header.jsonAdd a mega-nav Quick Access card
// src/data/megaMenu.ts
export const quickAccess: MegaLink[] = [
// ...
{
key: 'salesDashboard',
labelKey: 'salesDashboard', // nav:salesDashboard
descKey: 'qaSalesDesc', // nav:qaSalesDesc
to: '/dashboards/sales',
icon: TrendingUp,
accent: 'success',
},
]Best practices
- Keep the right cluster order stable. Users learn the icon positions; reorder only with intent.
- Never hardcode colours. Icon tiles use
accentTile(token classes); the whole bar uses semantic tokens (bg-surface/80,text-muted-foreground,border-border). See Design Tokens & Dark Mode. - Never hardcode user-facing text. All labels resolve through i18n (
header,nav,search,notificationsnamespaces). See Internationalization. - Don't put the sidebar tree in the header.
HeaderMegaNavis a curated showcase; the full tree lives in the sidebar (src/data/menu.ts). See Sidebar & Navigation. - Prefer the shared stores.
useTheme,usePresence,useLayoutare module-level stores so the header, customizer, and Layout Settings stay in sync — don't fork them to localuseState. - Gate desktop-only affordances with the existing responsive classes (
hidden sm:block,hidden lg:block) rather than removing controls.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Mega-nav triggers don't appear | config.showPrimaryNav is off and the route isn't header-only | Enable "Show primary nav" in the Customizer, or set headerOnly on the route. |
| ⌘K does nothing | The keydown listener lives in AppLayout — a modal may be swallowing the event | Confirm you're inside AppLayout (not /auth/*); check no other handler preventDefaults ⌘K first. |
| "Recent" group is empty in the palette | No routes visited yet this session/history | pushRoute records on navigation; navigate a few pages. Persisted in localStorage('recent-routes'). |
| Skin swatch shows the wrong colours | Missing data-skin scope around the swatch | Swatches must be wrapped in data-skin={id} to re-scope tokens (see DesignSystemMenu). |
| Header won't auto-hide | autoHideNav off, or stickyPageHeader on | They're mutually exclusive; enabling auto-hide clears sticky page header (and vice-versa). |
| Bell badge stuck | unreadCount derived from useNotifications state | Marking read updates the store; the seed is in-memory (resets on reload). |
| Sign-out doesn't clear session | Token key mismatch | Sign-out removes API_CONFIG.tokenKey; ensure your auth uses the same auth_token key. |
FAQ
Is the header nav the same as the sidebar? No. The sidebar (src/data/menu.ts) is the full app
tree; the header's HeaderMegaNav is a separate curated showcase (src/data/megaMenu.ts), kept in sync
by convention.
Where does the brand name come from? Only from src/config/brand.ts (nameLead / nameAccent).
The header brand lockup only renders in header-only mode.
How do I change the signed-in user shown in the profile menu? Edit currentUser in
src/data/user.ts (mock display data until a real auth/user context lands). Presence overrides persist
to localStorage('user-status').
Can I move an overlay's open-state into the header? Overlay open-state deliberately lives in
AppLayout so ⌘K and the panels survive across the header's own re-renders. Keep it there.
Why is <Toaster/> not in the header? Toasts are mounted at the root in main.tsx so they survive
route changes (e.g. sign-out → /auth/login).
Notes for designers & content editors
- Labels are i18n keys, not literals. Header controls read the
headernamespace; nav/mega labels readnav; palette text readssearch; the bell feed readsnotifications. Editsrc/locales/en/<ns>.jsonand translate via the i18n tooling. - Icons are
lucide-react. Swap the icon in the relevant data file (apps.ts,megaMenu.ts) or component; keep the same visual weight (h-5 w-5in the cluster). - Colours come from tokens/skins, not hex. To recolour an accent tile, change its
accentvalue (one of the five semantic tones), not a class. - Flags are SVGs in
public/flags/<code>.svg; the active flag drives the language trigger. - Avatars are self-hosted in
public/avatars/. The profile uses/avatars/avatar-default.pngby default.
Related
Layout & Shell
The configurable shell, LayoutContext, route presentation.
Sidebar & Navigation
The full nav tree, favorites, badges, modes.
Customizer & Settings
Where header flags are toggled.
Design Skins
The skin mechanism behind DesignSystemMenu.
Internationalization
The language switcher and locale files.
Was this page helpful?
