Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.encoreos.io/llms.txt

Use this file to discover all available pages before exploring further.

Version: 2.0.0 Last Updated: 2026-04-19 Purpose: Detailed guide for implementing mobile navigation patterns in Encore Health OS
Navigation documentation index: NAVIGATION_GUIDE_INDEX.md
Major changes vs v1.4.0:
  • Removes references to MobileMenuSheet (consolidated into MobileNavMore per ResponsiveNav.tsx 2026-Q1).
  • Updates the component map to reflect the current MobileNav (4-slot dock + More drawer), MobileNavMore, MobileNavDockSheetContent, MobileSidebarNav (feature-flagged).
  • Adds the new BottomTabBar shared primitive for core-level bottom navs (CE today; HR/RH later).
  • Aligns CSS-var examples with production (--mobile-nav-height: 76px, --mobile-header-height: 48px, --mobile-header-context-height: 32px).
  • Codifies the mobile-component-location convention (cores//components/mobile/).
  • Edge-swipe back is now bound to the main scroll area (PR-3.2 in the mobile-gestures consistency plan), not just the header.

Overview

Encore Health OS uses a mobile-first navigation system:
  • Bottom tab bar for primary navigation (< 768 px).
  • Top header with logo, search, notifications, and a contextual primary action.
  • Mobile breadcrumbs (last 2 segments) for hierarchical context.
  • More drawer (MobileNavMore + MobileNavDockSheetContent) for secondary navigation, themes, sign-out, and module switching.
  • Desktop: Sidebar (SidebarProvider + AppSidebar + SidebarInset) at ≥ 768 px. Other nav modes (Dock, Taskbar) live in NavModeContext.

Quick Reference

Component map (current)

ComponentPathPurpose
ResponsiveNavsrc/platform/navigation/ResponsiveNav.tsxTop-level mobile/desktop switch
MobileNavsrc/platform/navigation/MobileNav.tsx4-slot bottom dock with More button
MobileNavMoresrc/platform/navigation/MobileNavMore.tsxSheet trigger for the More drawer
MobileNavDockSheetContentsrc/platform/navigation/MobileNavDockSheetContent.tsxDrawer body (modules, settings, theme, sign-out)
MobileHeadersrc/platform/navigation/MobileHeader.tsxTop app bar (logo, back, search, notifications)
MobileBreadcrumbssrc/platform/navigation/MobileBreadcrumbs.tsxCompact 2-segment crumb row
MobileOrgSwitchersrc/platform/navigation/MobileOrgSwitcher.tsxMulti-org switcher in context row
MobileSidebarNavsrc/platform/navigation/MobileSidebarNav.tsxFeature-flagged (pf.mobile_sidebar_nav) full-tree drawer
MobileModuleSwitchersrc/platform/modules/MobileModuleSwitcher.tsxModule grid inside the More drawer
MobileModuleIconsrc/platform/modules/MobileModuleIcon.tsxModule icon helper
BottomTabBarsrc/platform/navigation/components/BottomTabBar.tsxReusable mobile bottom-tab primitive (module-level navs)
useModuleBottomTabssrc/platform/navigation/hooks/useModuleBottomTabs.tsDerives bottom-bar tabs from MODULE_REGISTRY navGroups
MobileModuleNavStrip (new)src/platform/navigation/components/MobileModuleNavStripImpl.tsxScrollable chip bar for in-module section switching
useRoleBasedDefaults (new)src/platform/navigation/hooks/useRoleBasedDefaults.tsRole-based default shortcut presets
useNavigationFrequency (new)src/platform/navigation/hooks/useNavigationFrequency.tsTracks module visit frequency for suggestions
MobileMenuSheet was removed in 2026-Q1 — its responsibilities are now in MobileNavMore + MobileNavDockSheetContent.

Context-aware bottom navigation (2026-Q2)

MobileNav now operates in two modes:
  1. Platform mode (default): Shows the user’s 3 customizable shortcut slots + More. Active when no module is detected.
  2. Module mode: When the user is inside a module (detected via useModuleRouting), the bottom bar automatically transforms to show that module’s top navigation sections (derived from MODULE_REGISTRY.navGroups via useModuleBottomTabs), plus the More button.
The More drawer is also context-aware: when opened from inside a module, it auto-navigates to that module’s L2 groups instead of the top-level modules grid (initialModuleId prop). Dead core-level MobileBottomNav files in CL, PM, and HR were removed in 2026-Q2. CE’s MobileBottomNav / MobileCrmShell remain for CE-specific features (QuickLogFAB, offline indicator) but the CE tab bar will be superseded by the platform context-aware pattern.

Common patterns

// Platform mobile nav (auto-rendered by ResponsiveNav; do not embed manually)
<ResponsiveNav user={user}>
  <Routes />
</ResponsiveNav>

// Module bottom tabs are now auto-generated from MODULE_REGISTRY:
// - useModuleBottomTabs(currentModule) derives up to 3 tabs from navGroups
// - MobileNav renders them when isModuleRoute is true
// No manual BottomTabBar usage needed for new modules.

// CE legacy bottom nav (still used for QuickLogFAB/offline; will be migrated)
<BottomTabBar
  ariaLabel="CE mobile navigation"
  tabs={CE_MOBILE_TABS}
/>

Key requirements

  • Touch targets: ≥ 44 × 44 px (WCAG 2.1 AAA, Apple HIG). Use min-w-[44px] min-h-[44px].
  • Safe areas: all fixed surfaces apply env(safe-area-inset-*) via the CSS vars below.
  • Breakpoints: mobile < 768 px, desktop ≥ 768 px. Use useIsMobile() from @/shared/lib/hooks/use-mobile (do not use useMediaQuery for the mobile breakpoint).
  • Z-index: bottom nav uses z-50; app-lock overlay (PF-79) wins at z-[100].
  • Content padding: the <main> inside ResponsiveNav already pads top + bottom for header/nav heights using CSS vars.

Common issues

SymptomFix
Content hidden behind bottom navThe mobile <main> already adds pb-[calc(var(--mobile-nav-height,64px) + var(--safe-area-inset-bottom,0px))]. If you bypass it, add the same padding.
Safe area not honoredMake sure the viewport meta tag has viewport-fit=cover (✅ in index.html).
Touch target too smallAdd min-w-[44px] min-h-[44px] to the button.
Hydration flash on mobileuseIsMobile now reads window.innerWidth synchronously on mount (PR-3.1) — verify your code calls the hook from @/shared/lib/hooks/use-mobile.

CSS Variables (Single Source of Truth)

Defined in src/index.css:
:root {
  --safe-area-inset-top: env(safe-area-inset-top, 0px);
  --safe-area-inset-bottom: env(safe-area-inset-bottom, 0px);
  --safe-area-inset-left: env(safe-area-inset-left, 0px);
  --safe-area-inset-right: env(safe-area-inset-right, 0px);
  --mobile-header-height: 48px;
  --mobile-header-context-height: 32px;
  --mobile-nav-height: 76px;
  --dock-height: 72px;
}
Helper utility classes (also in index.css):
  • .safe-area-top / .safe-area-bottom / .safe-area-left / .safe-area-right
  • .no-pull-refresh (overscroll-behavior-y: contain)
  • .scrollbar-thin / .scrollbar-x / .scrollbar-y for scroll-area styling

1. Architecture

ResponsiveNav (handles mobile/desktop switching via useIsMobile)
├── Mobile (< 768px)
│   ├── MobileHeader (top app bar)
│   ├── <main> with pt/pb computed from CSS vars
│   ├── MobileNav (bottom dock — 4 slots + More)
│   │   └── MobileNavMore → MobileNavDockSheetContent
│   ├── ContextualActionFAB (above bottom nav)
│   └── (optional) MobileSidebarNav (feature flag pf.mobile_sidebar_nav)
└── Desktop (>= 768px)
    └── SidebarProvider + AppSidebar + SidebarInset
The useEdgeSwipe for back navigation is bound to the mobile main scroll container (PR-3.2), so it can fire from anywhere in the left 20 px of the viewport — not just the 48 px header.

2. Bottom Navigation

Platform MobileNav

The platform-level bottom nav lives at src/platform/navigation/MobileNav.tsx and renders 4 slots + a More button:
[shortcut 1] [shortcut 2] [shortcut 3] [shortcut 4] [More]
Shortcuts come from useMobileNavPreferences (per-user) filtered against permission via useMobileNavAllowedShortcuts. The list is configured in mobile-nav-config.ts. Long-pressing the More button opens the Customize dialog. Active-state visuals:
  • A sliding pill underline animates between the active item.
  • aria-current="page" is set on the active link.

Core-level BottomTabBar (new)

For modules that need a module-scoped bottom nav (today only CE; potentially HR / RH for field staff), use the shared primitive at src/platform/navigation/components/BottomTabBar.tsx:
import { BottomTabBar } from '@/platform/navigation/components/BottomTabBar';

<BottomTabBar
  ariaLabel="CE mobile navigation"
  tabs={[
    { key: 'dashboard', label: 'Dashboard', icon: BarChart3, path: '/ce', permission: 'ce.dashboard.view' },
    { key: 'contacts',  label: 'Contacts',  icon: Users,     path: '/ce/contacts', permission: 'ce.contacts.view' },
    { key: 'partners',  label: 'Partners',  icon: Building2, path: '/ce/partners', permission: 'ce.partners.view' },
    { key: 'activities', label: 'Activities', icon: Phone,    path: '/ce/activities', permission: 'ce.activities.view' },
  ]}
/>

Stacking with the platform nav

A core-level BottomTabBar does not replace the platform MobileNav — both can render. To avoid double-stacking:
  • Either offset the core nav above the platform nav (default), or
  • Hide the platform nav for the route by checking useNavigation().isSubModuleRoute and conditionally rendering. (Decision: keep both for now; CE has confirmed the dual-nav UX.)

3. More Drawer (MobileNavMore + MobileNavDockSheetContent)

Replaces the legacy MobileMenuSheet. Contents:
  1. User profile header with avatar.
  2. Notifications inbox link.
  3. AI Assistant launch.
  4. Theme toggle.
  5. Phone / Log Call (RingCentral integration).
  6. All modules grid (collapsible by category).
  7. Search.
  8. Sign out.
To customize the drawer, do not embed it manually — it’s instantiated inside MobileNav.

4. Safe Area Insets

Already wired into index.css. Consumers reference the CSS vars:
<nav style={{ paddingBottom: 'var(--safe-area-inset-bottom, 0px)' }}>
or with the helper class:
<nav className="safe-area-bottom">

Test devices

  • iPhone X / XS / 11 / 12 / 13 / 14 / 15 (notched + home indicator)
  • iPhone 14 / 15 Pro (Dynamic Island)
  • Pixel 6 / 7 / 8 (home indicator)
  • iPad Pro / iPad Air

5. Touch Targets

<Button className="min-h-[44px] min-w-[44px]" aria-label="Open menu">
  <Menu className="size-5" aria-hidden="true" />
</Button>
Spacing: gap-2 (8 px) minimum between adjacent targets.

6. Mobile-Component Location Convention (new)

Cores have grown three different conventions for “where do mobile-only components live?”:
  • CE: dedicated src/cores/ce/components/mobile/ folder.
  • CL / HR: Mobile<Name>.tsx colocated with desktop peer.
  • FM / LO / RH: nothing (all responsive in one file).
Going forward, use one of these two patterns (in order of preference):
  1. Mobile-only components that don’t have a 1:1 desktop sibling → put under src/cores/{core}/components/mobile/.
  2. Mobile variants of existing components (small) → colocate as <Name>.mobile.tsx peer to <Name>.tsx.
Naming:
  • Mobile<Domain><Surface> (e.g. MobileVitalsKeypad, MobileBottomNav).
  • Use Sheet not Drawer / Modal in new file names (per root AGENTS.md). Existing names with Drawer are grandfathered until renamed (see MobileVitalsDrawer → MobileVitalsSheet rename in PR-2.5).
This convention is also pinned in src/cores/AGENTS.md and per-core AGENTS files where mobile work is active.

7. Edge Swipe Back Navigation (updated)

The hook (useEdgeSwipe) is now bound to the main mobile scroll container in ResponsiveNav rather than only MobileHeader. This means a left-edge swipe anywhere in the visible viewport — not just the 48 px header — triggers back navigation.
// ResponsiveNav.tsx (mobile branch)
const edgeSwipe = useEdgeSwipe({
  onBack: () => navigate(-1),
  enabled: routeDepth >= 2,
  edgeWidth: 20,    // 20 px from left edge
  threshold: 100,   // 100 px swipe to commit
});
Visual indicator (1 px primary-color bar) overlays the viewport when the gesture is active. If a page does not want edge-swipe back (e.g. a horizontally-scrollable canvas), wrap the page root in a div with style={{ touchAction: 'pan-x' }} to opt out.

8. Offline Navigation

Use the existing useOnlineStatus hook. The MobileHeader shows an offline banner; MobileNav already hides routes that require online when offline.
import { useOnlineStatus } from '@/shared/hooks/useOnlineStatus';
const isOnline = useOnlineStatus();
PWA service worker caches the shell + critical assets per vite.config.ts Workbox config; navigation falls back to the cached shell when offline.

9. Navigation Error Boundaries

Use the platform RouteErrorBoundary (src/platform/navigation/components/RouteErrorBoundary.tsx) — wraps each route module. Reports to Sentry; exposes a “Retry” + “Go home” UI.

10. Testing Checklist

Functional

  • Bottom nav appears on mobile (< 768 px), hidden on desktop.
  • More drawer opens/closes.
  • Edge swipe back navigates one level (route depth ≥ 2).
  • Customize dialog (long-press More) saves preferences.
  • Core-level BottomTabBar filters by permission.

Safe areas

  • iPhone X+ / Pro models — no overlap with notch / Dynamic Island.
  • Pixel 7+ — bottom nav above home indicator.
  • Landscape orientation — left/right insets honored.

Touch targets

  • All bottom-nav buttons ≥ 44 × 44 px.
  • Header icon buttons ≥ 44 × 44 px.
  • Drawer rows ≥ 44 px tall.

Accessibility

  • aria-current="page" on active tab.
  • aria-label on every icon-only button.
  • Esc closes the More drawer.
  • Skip-to-content link works from keyboard.
  • Keyboard arrow keys navigate between bottom-nav slots (MobileNav already implements this).

Hydration / first paint

  • No desktop chrome flash on real mobile devices (PR-3.1 fix in useIsMobile).
  • Boot splash transitions cleanly.

Cross-browser

  • iOS Safari 14+ ✅
  • Android Chrome 90+ ✅
  • Firefox / Edge mobile

11. Mobile Breadcrumbs

MobileBreadcrumbs shows the last 2 segments of the route. Detail/edit pages set the dynamic trailing crumb via useEntityBreadcrumb(data, (e) => e.name) from @/shared/lib/hooks/useEntityBreadcrumb. Static labels come from BASE_ROUTE_LABELS in src/platform/navigation/route-labels.ts (audit: npm run audit:routes-navigation).
useEntityBreadcrumb(patient, (p) => `${p.last_name}, ${p.first_name}`);
Pages MUST NOT import @/shared/ui/breadcrumb primitives directly — header auto-render is the standard.

12. Best Practices Summary

  1. Use CSS vars (--mobile-nav-height, --safe-area-inset-*) for all fixed-element spacing; never hard-code.
  2. Keep touch targets at 44 × 44 px minimum.
  3. Use useIsMobile for the 768-px breakpoint; useMediaQuery is reserved for non-standard breakpoints and is @deprecated for the mobile case.
  4. Place mobile-only components under src/cores/{core}/components/mobile/.
  5. Use the Sheet primitive for slide-up panels (not “Drawer” or “Modal” in new code).
  6. Use BottomTabBar for core-level mobile navs (CE today; HR/RH if needed).
  7. Use SwipeableCardShell (from @/platform/gestures) instead of hand-rolled *Swipeable.tsx wrappers.
  8. Set aria-current="page" on active tabs and aria-label on icon-only buttons.
  9. Honor prefers-reduced-motion automatically — Phase 1 gesture hooks already do this (PR-3.3).
  10. Test on real notched iOS + home-indicator Android devices (or Chrome DevTools device emulation).

13. Quick Actions Pattern (PF-20)

Quick actions are not in mobile navigation. They appear on overview pages via QuickActionsSection from @/platform/dashboard/components/QuickActionsSection. The mobile More drawer surfaces a “Quick Actions” entry that opens the command palette.

14. Mobile Swipe Gestures

See Mobile Gesture Guide v2.0.0 (companion document) for the complete catalog of swipe / pinch / multi-touch hooks and components, including SwipeableCardShell, MobilePullToRefresh, useEdgeSwipe, and Phase 2 user preferences.

15. References


Document status: Active Maintained by: Platform Team Last reviewed: 2026-04-19