Skip to content

Mobile Navigation

Loquent replaces the desktop sidebar with a native-feeling navigation stack on mobile: a persistent bottom tab bar, a WhatsApp-style nav header on detail routes, direction-aware page transitions, and hardware back-button support. Desktop UI is completely unaffected.

MobileTabBar renders a fixed bottom bar on root-level routes (lists, dashboards). It hides automatically on detail views.

File: src/mods/mobile/components/mobile_tab_bar.rs

TabIconDestination
HomeHomeWorkspace
MessagesMessageSquareMessaging
CallsPhoneCalls
TasksCheckSquareTasks
MoreMoreHorizontalOpens MoreSheet

The active tab shows a bg-primary/15 pill behind the icon with an animated scale transition. Each tab enforces the 44px touch-target floor (min-h-11 min-w-11).

The tab bar renders only when is_root_route(&route) returns true. Root routes include list views (Workspace, Messaging, Calls, Tasks, Contacts, Settings, Admin). Detail and create routes hide the tab bar.

When the keyboard opens, the JS bridge adds .kb-open to <html>, which hides the tab bar via CSS:

html.kb-open .mobile-tab-bar {
display: none;
}

The bar uses a frosted-glass effect: bg-background/35 backdrop-blur-2xl backdrop-saturate-150, pinned at z-30 with pb-safe for notch clearance.

The “More” tab opens a bottom drawer with permission-gated links to secondary destinations: Dashboard, Insights, Contacts, Reports, AI tools (Agents, Knowledge, Analyzers), Sales (Offers, Products), Automation (Plan Templates, Plans, Widgets), Settings, and Admin.

MobileNavHeader replaces the desktop back button on detail routes with a WhatsApp-style header: left chevron, optional avatar, title/subtitle, and right actions.

File: src/shared/components/mobile_nav_header_component.rs

pub struct MobileNavHeaderProps {
pub title: String,
pub subtitle: Option<String>,
pub back_to: Option<NavigationTarget>,
pub leading: Option<Element>, // e.g. avatar
pub right_action: Option<Element>, // e.g. edit/call buttons
}
MobileNavHeader {
title: contact_name,
subtitle: last_seen_text,
back_to: Route::ContactListView { query: ContactFilterQuery::default() },
leading: rsx! { Avatar { src: contact.avatar_url, size: 32 } },
right_action: rsx! {
button { class: "min-h-11 min-w-11", Phone { size: 20 } }
},
}

When back_to is None, the chevron calls nav.go_back(). When set, it navigates to the specified route. ViewContainer auto-injects the mobile header when back_button is present.

MobilePageTransition wraps Outlet and animates route changes based on navigation direction — slide from right on push, slide from left on pop, cross-fade on peer swap.

File: src/mods/mobile/components/mobile_page_transition.rs

The use_mobile_nav hook (wired in AppLayout) compares route_depth of the previous and current routes:

Depth changeDirectionAnimation
IncreasesPushSlide from right (320ms)
DecreasesPopSlide from left (280ms)
SameFadeCross-fade (200ms)
InitialNoneNo animation

All animations use cubic-bezier(0.32, 0.72, 0, 1) and respect prefers-reduced-motion: reduce.

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NavDirection {
None,
Push,
Pop,
Fade,
}

route_meta.rs classifies routes for tab bar visibility, active tab highlighting, and transition direction.

File: src/mods/mobile/utils/route_meta.rs

FunctionReturnsPurpose
is_root_route(&route)boolWhether the bottom tab bar shows
active_tab(&route)MobileTabWhich tab to highlight
route_depth(&route)u8Depth for transition direction (0 = root, 1 = detail, 2 = sub-section)
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MobileTab {
Home,
Messages,
Calls,
Tasks,
More,
}

use_capacitor_back_button listens for the Android hardware back button and calls nav.go_back() on non-root routes. On root routes, the listener is inactive — the user navigates via the tab bar.

File: src/shared/capacitor/back_button.rs

use_capacitor_back_button(|| {
let r = router().current::<Route>();
!is_root_route(&r)
});

Native WKWebView handles swipe-back gestures — no custom code needed.

All mobile navigation components are wired in AppLayout:

src/shared/layouts/app_layout.rs
use_capacitor_back_button(|| !is_root_route(&router().current::<Route>()));
use_mobile_nav(); // Provides MobileNavContext
// Main content area
main {
class: "flex-1 overflow-y-auto bg-background pb-mobile-tab md:pb-0 pl-safe pr-safe",
MobilePageTransition {} // Wraps Outlet
}
MobileTabBar {}

The pb-mobile-tab utility adds bottom padding equal to the tab bar height plus safe area:

@utility pb-mobile-tab {
padding-bottom: calc(2.75rem + env(safe-area-inset-bottom, 0px));
}

On desktop (md:pb-0), this padding is removed.

FilePurpose
src/mods/mobile/mod.rsModule root, constants, exports
src/mods/mobile/components/mobile_tab_bar.rsBottom tab navigation
src/mods/mobile/components/more_sheet.rsOverflow destinations drawer
src/mods/mobile/components/mobile_page_transition.rsDirection-aware route animation
src/mods/mobile/hooks/use_mobile_nav.rsNavigation direction tracker
src/mods/mobile/utils/route_meta.rsRoute depth, tab mapping, root detection
src/shared/components/mobile_nav_header_component.rsWhatsApp-style detail header
src/shared/capacitor/back_button.rsAndroid hardware back handler
src/shared/layouts/app_layout.rsWires tab bar, transitions, back button