Skip to content

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:

ApplicationPath prefixNav 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

LayerShared?Notes
layouts/app-layout.tsxYesIdentical shell for both apps
layouts/auth-layout.tsxYesGuest auth screens
components/layout/*YesReads context from pathname; no Central fork
components/ui/*YesPrimitives (button, card, dialog, table, …)
components/common/*YesPageHeader, EmptyState, LoadingState, ErrorState
components/dashboard/*Mostly Central widgetsWelcomeHero / QuickActions / TenantsOverviewTable stay Central-specific
Page contentPer appTenant 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 in App.tsx inside 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 with renderOverview field 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)

RoutePage
/dashboardTenant dashboard (widget registry: pipeline, tasks, activity, notifications, quick actions)
/leadsLeads Kanban/table; create/view/edit on dedicated pages
/tasksTasks board/list; create/view/edit on dedicated pages
/settingsWorkspace Settings (General / Branding / Mail)
/profileShared 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

  1. Add the path to tenantRoutes in src/config/routes.ts.
  2. Add a NavigationItem under the catalog-aligned group in tenantNavigationGroups (CRM, Communication, Sales, Billing, Purchasing, Inventory, Operations, Finance, HR) with permission and module slug. Do not reintroduce a catch-all Workspace group.
  3. Register list + /new + /:id + /:id/edit under TenantProtectedRouteAppLayout in App.tsx.
  4. 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 apiContext so the axios interceptor uses getAuthContext() 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

Official documentation for the EloSync SaaS Platform.