- Role-filtered sidebar (Owner/Admin/User visibility rules BR-U3-01–06) - Topbar removed; UserMenu + LanguageSwitcher move to sidebar footer - Mobile slide-over sidebar with hamburger button - Theme toggle (light/dark) with localStorage persistence and no-flash init - New components: MobileBar, SidebarOverlay, ThemeToggle, useTheme - Unit 2 marked complete in aidlc-state Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
1.8 KiB
1.8 KiB
Business Logic Model — Unit 3: Layout & Navigation
Component Hierarchy
AppLayout
├── Sidebar (desktop: always visible)
│ ├── Logo / AppName
│ ├── NavList (role-filtered NavItems)
│ │ └── NavItem (Link with activeProps)
│ └── SidebarFooter
│ ├── LanguageSwitcher
│ ├── ThemeToggle
│ └── UserMenu (name, email, logout)
├── MobileBar (mobile only — hamburger + app name)
│ └── opens → SidebarOverlay (Sidebar rendered in overlay)
└── <main>
└── <Outlet />
Theme Initialisation (no flash)
Theme is applied in a blocking inline script in index.html — before React hydrates — to prevent a flash of the wrong theme:
<script>
(function() {
var stored = localStorage.getItem('cms-theme');
var theme = stored || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
document.documentElement.classList.add(theme);
})();
</script>
React's useTheme hook reads from localStorage on mount and keeps the toggle in sync.
NavItem Filtering Logic
const visibleItems = NAV_ITEMS.filter(item =>
!item.roles || item.roles.includes(user.role)
);
item.roles being undefined means "visible to everyone". This is evaluated at render time whenever user.role changes.
Mobile Sidebar State
Local useState<boolean> in AppLayout (or MobileBar). The sidebar overlay uses a <dialog> or a Tailwind-animated translate-x panel. Closes on:
- Backdrop click (
onBackdropClick) - Close button click
- TanStack Router navigation (via
useEffectwatchinglocation.pathname)
Logout Flow (unchanged from Unit 1/2)
UserMenu in sidebar footer calls useAuth().logout() then navigate({ to: '/login' }).