Architecture & Routing
How the Luminaux app boots, how routes are defined, and how state and persistence are organized.
The application has three architectural pillars, and understanding them makes the rest of the codebase predictable:
- A provider tree (
src/main.tsx) that supplies layout config, motion config, and toasts. - A single routing source of truth (
src/routes.tsx) that also declares per-route presentation. - A file-per-feature data layer (
src/data/*.ts) with a consistent seed + load/save/clear persistence pattern.
The provider tree
src/main.tsx mounts the app under StrictMode with this nesting:
// src/main.tsx (shape)
createRoot(document.getElementById('root')!).render(
<StrictMode>
<BrowserRouter>
<LayoutProvider> {/* shell config, skin, OLED — src/context/LayoutContext.tsx */}
<MotionProvider> {/* global reduced-motion config — src/components/motion */}
<ToastProvider> {/* toast state — src/components/ui/Toast */}
<App />
<AppSplash /> {/* branded first-load splash */}
<Toaster /> {/* mounted at root so toasts survive route changes */}
</ToastProvider>
</MotionProvider>
</LayoutProvider>
</BrowserRouter>
</StrictMode>,
)Key point: <Toaster /> lives at the root, not inside AppLayout, so toasts persist across navigation
(e.g. a sign-out toast that outlives the route change). ChatProvider is mounted lower, inside AppLayout,
because the chat widgets only exist within the shell.
StrictMode gotcha
In development, StrictMode mounts → unmounts → remounts each component. Any "is-mounted" ref must be set true
on mount (not only false on cleanup), or it stays stuck false.
Pre-paint scripts (no flash)
index.html contains an inline script that runs before React, reading localStorage to apply the correct
theme/skin/language on the very first paint (avoiding a flash of the wrong theme):
- adds
.darkto<html>whentheme === 'dark'(orsystem+ OS dark); - validates and sets
data-skin(falls back todefaultif the saved value isn't in the allowlist); - sets
data-oledwhenoled_dark === 'true'; - sets
<html lang>from the savedlanguage(whitelisted to the supported languages).
Because these attributes are set pre-paint, the token system resolves the right colors immediately.
The routing model
src/routes.tsx is the single source of truth for real pages. It exports a typed array:
// src/routes.tsx (shape)
export interface AppRoute {
path: string
element: React.ReactNode
fullBleed?: boolean // drop <main> padding; page owns its height
headerOnly?: boolean // hide the sidebar; nav moves to the header
fixedFooter?: boolean // pin the footer
hideWidgets?: boolean // suppress chat widgets on this route
}
export const appRoutes: AppRoute[] = [
{path: '/dashboards/analytics', element: <AnalyticsDashboard />},
{path: '/apps/chat', element: <ChatPage />, fullBleed: true, headerOnly: true, fixedFooter: true},
{path: '/ui/panels', element: <PanelsPage />},
// …~90 entries, including two data-driven folds (see below)
]Two route groups are folded in from data arrays instead of being hand-listed, so their source of truth stays with the feature:
- Page-Layout previews —
...LAYOUT_PREVIEWS.map(...)turns the preview descriptor array (src/pages/layouts/LayoutPreviewPage.tsx) into 20/page-layouts/*routes, all rendering the shared<LayoutPreviewPage variant={…}/>template. - Email templates —
...EMAIL_TEMPLATES.map(...)turns the descriptor array insrc/data/emailTemplates/meta.tsinto 12 lazy, full-bleed/email/*routes. The raw email HTML ships in that page's lazy chunk, not the main bundle.
src/App.tsx then:
- maps
appRoutesto<Route>elements (lazy ones wrapped in<Suspense fallback={<PageLoader/>}>); - renders the skin-dependent
/index from its own module:HomeRoute()returns<BentoDashboard/>when the skin isbento,<ConsoleDashboard/>whenconsole, else<CrmDashboard/>; - splits the flattened menu leaves into shell leaves (anything not covered by
appRoutesfalls back to a titled<Placeholder>) and auth leaves (/auth/*), which render outside the<AppLayout>route.
To add a real page
A route being present in appRoutes is what makes it "real". See
Getting Started → Adding a new page.
Standalone auth routes (authRoutes)
Auth pages render standalone — outside the AppLayout shell (no sidebar/header/footer), so they are
not in appRoutes. src/routes.tsx exports a second map:
// src/routes.tsx (shape)
export const authRoutes: Record<string, ReactNode> = {
'/auth/login': <LoginPage />,
'/auth/login-v2': <LoginV2Page />,
'/auth/register': <RegisterPage />,
'/auth/register-v2': <RegisterV2Page />,
'/auth/forgot-password': <ForgotPasswordPage />,
'/auth/reset-password': <ResetPasswordPage />,
'/auth/lock-screen': <LockScreenPage />,
}App.tsx maps each /auth/* menu leaf to authRoutes[l.to] ?? <Placeholder …/> as a sibling of the
<AppLayout> route — so an auth page owns the whole viewport, and any future /auth/* menu leaf without an
entry still falls back to a standalone placeholder. Paths must match the menu (src/data/menu.ts). The seven
screens are covered in Authentication.
Per-route presentation flags
The presentation flags on an appRoutes entry are surfaced through useRouteLayout()
(src/layout/routeLayout.ts), which builds a routePresentation map and lets the shell force a layout for
specific routes without touching the user's saved config. Examples in the codebase:
/apps/chat→fullBleed + headerOnly + fixedFooter(the chat page owns the whole viewport and supplies its own rail)./apps/pricing→fullBleed + headerOnly + hideWidgets(a marketing landing page —hideWidgetssuppresses the chat rail/bubble there)./apps/scrumboard,/apps/calendar,/apps/contacts→fullBleed.- The 12
/email/*template routes and the 3 immersive/error/*pages →fullBleed.
useRouteLayout() returns {forceHeaderOnly, forceFixedFooter, forceHideWidgets, fullBleed}, consumed by
AppLayout and Header. Full-bleed pages do not render <PageHeader> (they call useDocumentTitle
directly) and size themselves with calc(100dvh - var(--app-header-height) - <footer>).
Placeholders & the catch-all
The placeholder mechanism keeps the menu and the router from drifting. App.tsx computes the difference:
// Conceptually:
const explicit = new Set(['/', ...appRoutes.map(r => r.path)])
// Every menu leaf whose `to` is NOT explicit renders a titled <Placeholder> instead —
// inside the shell for normal routes, standalone for /auth/* routes.As of today, every menu leaf resolves to a real page — the mechanism stays as a safety net: add a menu leaf
before its route exists and it renders a friendly titled "coming soon" screen (src/pages/Placeholder.tsx)
instead of breaking navigation. A catch-all <Route path="*"> renders a pages:pageNotFound placeholder (the
designed, immersive 404 lives at /error/404).
The data layer
Feature data lives in src/data/<feature>.ts, each following the same shape:
// src/data/<feature>.ts (pattern)
export const STORAGE_KEY = 'feature-state-v1'
export interface FeatureState { /* … */ }
export const seed: FeatureState = { /* … */ }
export function loadX(): FeatureState { /* read localStorage, fall back to seed */ }
export function saveX(state: FeatureState): void { /* write localStorage */ }
export function clearX(): void { /* remove the key */ }Modules that follow this: chat.ts (chat-state-v2), calendar.ts (calendar-state-v2), scrumboard.ts
(scrumboard-state-v1), contacts.ts (contacts-state-v1), invoice.ts (invoice-state-v1), cookies.ts
(cookie-consent-v1). Static (non-persisted) data: menu.ts, navBadges.ts, apps.ts, quickCreate.ts,
megaMenu.ts, user.ts, notifications.ts, pricing.ts, settings.ts, faq.ts, countries.ts, and the
emailTemplates/ folder.
Feature view-models are hooks. Large app pages keep state/logic in a co-located hook and stay render-only:
useBoard (scrumboard), useCalendar (calendar), useApplicants (contacts), useChatPanes/useListMode
(chat). Follow this pattern for new complex pages.
Persistence & Reset
Every persisted key is registered in src/lib/appStorage.ts:
// src/lib/appStorage.ts (shape)
export const APP_LOCAL_KEYS = [
'layout_config', 'design_skin', 'oled_dark', 'theme', 'language',
'nav-favorites', 'user-status', 'recent-routes',
'chat-rail-collapsed', 'chat-state-v2', 'chat-list-mode',
'emoji-mart.frequently', 'emoji-mart.last',
'contacts-state-v1', 'scrumboard-state-v1', 'calendar-state-v2',
'invoice-state-v1', 'cookie-consent-v1',
// + a few legacy keys kept so Reset also cleans up older versions' state
] as const
export const APP_SESSION_KEYS = ['app-splash-shown'] as const
export function clearAppStorage() { /* removes every key above — but NOT auth_token */ }"Reset to defaults" (in the Customizer + Layout Settings) calls clearAppStorage() then reloads, so every
store re-hydrates from defaults + seed while the user stays signed in (auth_token is deliberately preserved).
Rule
Any time you add a persisted key, add it to APP_LOCAL_KEYS / APP_SESSION_KEYS so Reset wipes it. Prefer an
exported STORAGE_KEY constant over an inline string.
Best practices
- Add real pages via
appRoutes— never hand-maintain a parallel list of "real" routes. - Keep page components render-only; push state into a co-located
useXhook when it grows. - Force per-route layout with presentation flags, not by mutating
LayoutContext. - Register every new
localStorage/sessionStoragekey inappStorage.ts.
Troubleshooting
| Symptom | Cause / fix |
|---|---|
| A menu item shows "coming soon" | Its to has no appRoutes (or authRoutes) entry — add the route to make it real. |
| A stuck boolean ref in dev only | StrictMode double-mount — set the ref true on mount, not just false on cleanup. |
| Reset didn't clear my new feature's state | The key isn't in appStorage.ts. Add it. |
| Full-bleed page has a scroll gap | Size it with calc(100dvh - var(--app-header-height) - <footer>); subtract the footer only when fixedFooter. |
Related
Getting Started
Install, scripts, environment, and adding a page.
Layout
LayoutContext config, the configurable shell, PageHeader.
Page Layouts
Sidebar modes, navigation, and layout previews.
Authentication
The standalone authRoutes pages.
Data & API Client
The HTTP client, storage registry, and shared helpers.
Was this page helpful?
