Shared Layout — Developer Guide
App: EloSync-Frontend (React 19 + Vite).
Shared layout architecture
Both applications render authenticated pages through the same AppLayout:
ErrorBoundary (root in main.tsx; stale-chunk deploy recovery)
└── HashRouter
├── ScrollToTop (window.scrollTo(0, 0) on pathname change)
└── AppLayout
├── Sidebar (context-aware nav + brand subtitle; ~w-60 expanded)
├── Topbar (h-14; collapse tip shows ⌘/Ctrl+B)
│ ├── Collapse / mobile menu
│ ├── Breadcrumbs
│ ├── Command palette trigger (⌘K)
│ ├── Theme toggle
│ ├── Notifications
│ ├── Settings shortcut
│ └── User menu
├── <Outlet /> (page content; compact padding)
├── GlobalShortcuts (mod+b sidebar)
└── CommandPalette (mod+k)Route modules use lazyWithDeployRecovery (see src/lib/spa-deploy-recovery.ts). After a production deploy, open tabs that hit a missing hashed chunk auto-reload once instead of showing the root “Something went wrong” screen.
Module list pages also register useModuleShortcuts (n create — not Ctrl+N, browser-reserved; mod+f module search).
Context is path-based:
| Application | Path prefix | Nav config |
|---|---|---|
| Tenant | / (e.g. /dashboard) | tenantNavigationGroups |
| Central | /central/* | centralNavigationGroups |
Helpers in src/config/routes.ts:
getAuthContext(pathname)dashboardPathFor/profilePathFor/settingsPathFor/loginPathFor
Helpers in src/config/navigation.ts:
getNavigationGroups(context)getNavigationItems(context)applicationLabelFor(context)→"Central Application"|"Tenant Application"
Layout inheritance
| Layer | Shared? | Notes |
|---|---|---|
layouts/app-layout.tsx | Yes | Identical shell for both apps |
layouts/auth-layout.tsx | Yes | Guest auth screens |
components/layout/* | Yes | Reads context from pathname; no Central fork |
components/ui/* | Yes | Primitives (button, card, dialog, table, …) |
components/common/* | Yes | PageHeader, EmptyState, LoadingState, ErrorState |
components/dashboard/* | Mostly Central widgets | WelcomeHero / QuickActions / TenantsOverviewTable stay Central-specific |
| Page content | Per app | Tenant dashboard renders widget registry from GET /dashboard; CRM modules use shared Kanban + design-system pages |
Do not duplicate Sidebar/Topbar for Tenant. Parameterize via navigation config + route helpers.
Reusable components (preferred)
Shell
AppLayout,Sidebar,Topbar,Breadcrumbs,UserMenu,CommandPalette,GlobalShortcuts,ThemeToggle,ScrollToTop(mounted inApp.tsxinside the router; resets window scroll on pathname change, not query-string filters)- Root
ErrorBoundary+installSpaDeployRecovery()/lazyWithDeployRecovery— one-shot reload when a post-deploy lazy chunk 404s - Hooks:
useHotkeys,useModuleShortcuts— see Module Development Guide
Page chrome
PageHeader,RecordPage,RecordSection,EntityRecordSheet/ModuleRecordSheet(list-side quick create/peek withrenderOverviewfield summary; billing docs view-peek only),FormSubmitSplit(separate Create / Create & View buttons — no dropdown; module extras like Create & Send or Post),LoadingState,ErrorState,EmptyState,WidgetContainer
Primitives
- Cards, tables, forms, dialogs, sheets, buttons from
components/ui
Placeholders
pages/common/placeholder-page.tsx— reserved module/settings screens
UI structure (Tenant)
| Route | Page |
|---|---|
/dashboard | Tenant dashboard (widget registry: pipeline, tasks, activity, notifications, quick actions) |
/leads | Leads Kanban/table; create/view/edit on dedicated pages |
/tasks | Tasks board/list; create/view/edit on dedicated pages |
/settings | Workspace Settings (General / Branding / Mail) |
/profile | Shared ProfilePage (API context from pathname) |
Central remains under /central/* with the same shell and its own navigation groups.
Adding a Tenant module to the sidebar
- Add the path to
tenantRoutesinsrc/config/routes.ts. - Add a
NavigationItemunder the catalog-aligned group intenantNavigationGroups(CRM,Communication,Sales,Billing,Purchasing,Inventory,Operations,Finance,HR) withpermissionandmoduleslug. Do not reintroduce a catch-all Workspace group. - Register list +
/new+/:id+/:id/editunderTenantProtectedRoute→AppLayoutinApp.tsx. - Ship list/create/view/edit UI mirroring Leads (not a long-lived
PlaceholderPage).
Nav visibility is filtered by installed module subscriptions and Spatie permissions. Sidebar sections collapse by default; the group that contains the current route stays open. Users can expand additional sections; that preference is stored per Central/Tenant context in localStorage.
Auth / API notes
- Profile update/password change omit forced
apiContextso the axios interceptor usesgetAuthContext()from the URL. - Tenant API base:
/api/tenant/v1+X-Tenant-Domain. - Central API base:
/api/central/v1. - Notifications: Echo private-channel updates are primary; unread count uses a 90-second fallback poll only while Echo is disconnected.
Explicit non-goals (shell)
- Divergent Central vs Tenant shell redesigns
- Per-module notification stacks outside Laravel notifications +
/notifications*APIs