fix(layout): hoist AppLayout into a persistent AppShell router layout (MAPPS-366) #439

Merged
nrupard merged 1 commit from fix/MAPPS-366-persistent-app-shell into main 2026-07-23 18:34:13 +02:00
Owner

Problem

Navigating between the user dashboard and an admin dashboard blanked the page text for about half a second (icons did not flicker). Root cause: every page wrapped its own AppLayout (top bar + sidebar + banners), while the only router layout was AuthGuard (just an Outlet). So each SPA navigation swapped the whole page component through the Outlet, tearing down and rebuilding the entire shell every time. The MAPPS-203 / MAPPS-250 / MAPPS-287 workarounds (pushing sidebar scroll / collapse / title state up to the App root to survive the remount) were all symptoms of this.

Fix

Hoist the chrome into a single persistent AppShell, mounted as a Dioxus #[layout] inside AuthGuard. It renders the top bar, sidebar, banners, toast and account-deleted overlays once and swaps only the routed subtree through Outlet, so the shell stays mounted across navigation and nothing blanks.

  • Pages render their body directly and set their title via a new use_page_title(title) hook, backed by a shared PageTitle signal provided at the App root (use_page_title_provider in main.rs).
  • TopBar reads that signal itself and syncs document.title, so a title change re-renders only the bar, never the page. This is what makes it loop-safe: a page and its ContentUnavailable / PermissionRequired branch can both call use_page_title without fighting over the signal.
  • Chromeless authenticated routes (/onboarding/profile, /dashboard/tv, /big/*) are hoisted above the #[layout(AppShell)] boundary so they keep rendering full-screen with no chrome.
  • The now-unused AppLayout component is removed; all 91 AppLayout { title, ... } call sites across the authenticated pages are converted. use_page_title is placed before every early return (rules of hooks); permission-gated and detail pages were checked individually.

Scope

Core: components/layout.rs, main.rs, lib.rs (route restructure), content_unavailable.rs, permission_state.rs. Plus the 24 authenticated page modules (mechanical wrapper removal). Chromeless (big_view, dashboard::DashboardTvPage) and portal pages are untouched.

Verification

just check-docker (full wasm release build) and just pre-commit (cargo fmt --check + cargo clippy --all-targets -- -D warnings + cargo check --target wasm32-unknown-unknown + cargo test --lib, 223 passing) are both green.

A render test (layout.rs) asserts the new use_page_title plumbing carries a page's title through the shared PageTitle signal to the TopBar-style reader. The complementary "the shell does not re-mount across navigation" property is a structural guarantee of the #[layout(AppShell)] router construct (a Dioxus #[layout] is one scope the router keeps mounted across its child routes), enforced by the route wiring in lib.rs rather than asserted at runtime.

#MAPPS-366

## Problem Navigating between the user dashboard and an admin dashboard blanked the page text for about half a second (icons did not flicker). Root cause: every page wrapped its own `AppLayout` (top bar + sidebar + banners), while the only router layout was `AuthGuard` (just an `Outlet`). So each SPA navigation swapped the whole page component through the Outlet, tearing down and rebuilding the entire shell every time. The MAPPS-203 / MAPPS-250 / MAPPS-287 workarounds (pushing sidebar scroll / collapse / title state up to the App root to survive the remount) were all symptoms of this. ## Fix Hoist the chrome into a single persistent `AppShell`, mounted as a Dioxus `#[layout]` inside `AuthGuard`. It renders the top bar, sidebar, banners, toast and account-deleted overlays once and swaps only the routed subtree through `Outlet`, so the shell stays mounted across navigation and nothing blanks. - Pages render their body directly and set their title via a new `use_page_title(title)` hook, backed by a shared `PageTitle` signal provided at the App root (`use_page_title_provider` in `main.rs`). - `TopBar` reads that signal itself and syncs `document.title`, so a title change re-renders only the bar, never the page. This is what makes it loop-safe: a page and its `ContentUnavailable` / `PermissionRequired` branch can both call `use_page_title` without fighting over the signal. - Chromeless authenticated routes (`/onboarding/profile`, `/dashboard/tv`, `/big/*`) are hoisted above the `#[layout(AppShell)]` boundary so they keep rendering full-screen with no chrome. - The now-unused `AppLayout` component is removed; all 91 `AppLayout { title, ... }` call sites across the authenticated pages are converted. `use_page_title` is placed before every early return (rules of hooks); permission-gated and detail pages were checked individually. ## Scope Core: `components/layout.rs`, `main.rs`, `lib.rs` (route restructure), `content_unavailable.rs`, `permission_state.rs`. Plus the 24 authenticated page modules (mechanical wrapper removal). Chromeless (`big_view`, `dashboard::DashboardTvPage`) and portal pages are untouched. ## Verification `just check-docker` (full wasm release build) and `just pre-commit` (`cargo fmt --check` + `cargo clippy --all-targets -- -D warnings` + `cargo check --target wasm32-unknown-unknown` + `cargo test --lib`, 223 passing) are both green. A render test (`layout.rs`) asserts the new `use_page_title` plumbing carries a page's title through the shared `PageTitle` signal to the `TopBar`-style reader. The complementary "the shell does not re-mount across navigation" property is a structural guarantee of the `#[layout(AppShell)]` router construct (a Dioxus `#[layout]` is one scope the router keeps mounted across its child routes), enforced by the route wiring in `lib.rs` rather than asserted at runtime. #MAPPS-366
fix(layout): hoist AppLayout into a persistent AppShell router layout (MAPPS-366)
All checks were successful
Check / fmt + clippy + tests (pull_request) Successful in 1m17s
Create release / Create release from merged PR (pull_request) Has been skipped
318c298933
Navigating between the user and admin dashboards blanked the page text for about half a second: every page wrapped its own AppLayout (top bar + sidebar + banners), so each SPA navigation re-mounted the entire shell through the Outlet and the browser repainted the whole tree. The cached SVG icons survived the repaint but the text did not, which is the reported flash.

Hoist the chrome into a single persistent AppShell mounted as a Dioxus #[layout] inside AuthGuard: it renders the top bar, sidebar, banners, toast and account-deleted overlays once and swaps only the routed subtree through Outlet, so the shell stays mounted across navigation and nothing blanks. Each page now renders its body directly and sets its title via a new use_page_title hook (a shared PageTitle signal provided at the App root); TopBar reads that signal itself, so a title change re-renders only the bar and never the page, which avoids a page/title render loop even when a page and its ContentUnavailable / PermissionRequired branch both set the title. The chromeless routes (onboarding, the /dashboard/tv wall view, the /big/* kiosk views) sit above the AppShell layout so they keep rendering full-screen without chrome.

Removes the now-unused AppLayout component and converts all 91 AppLayout call sites across the authenticated pages to use_page_title, keeping every page's title (including the dynamic detail-page titles) unchanged.

#MAPPS-366

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
nrupard deleted branch fix/MAPPS-366-persistent-app-shell 2026-07-23 18:34:13 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/mokosh-apps!439
No description provided.