- 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>
3.9 KiB
Frontend Components — Unit 3: Layout & Navigation
Modified Components
AppLayout.tsx (modify existing)
Path: frontend/src/components/layout/AppLayout.tsx
Remove Topbar import and render. Add MobileBar. Layout becomes:
<div className="flex min-h-svh">
<Sidebar /> {/* desktop only */}
<MobileBar onMenuOpen={...} /> {/* mobile only */}
<main className="flex-1 p-6" data-testid="app-main">
<Outlet />
</main>
</div>
Mobile open/close state lives here (or in MobileBar).
Sidebar.tsx (modify existing)
Path: frontend/src/components/layout/Sidebar.tsx
Changes:
- Add
roles?: Role[]toNavIteminterface - Filter
NAV_ITEMSbyuser.rolebefore rendering - Add
SidebarFootersection at bottom withLanguageSwitcher,ThemeToggle,UserMenu - Accept optional
onClose?: () => voidprop for mobile overlay close button data-testid="app-sidebar"retained
Nav items:
| Label key | Route | Roles | Icon |
|---|---|---|---|
nav.dashboard |
/dashboard |
all | LayoutDashboard |
nav.users |
/users |
Owner, Administrator | Users |
nav.settings |
/settings |
Owner | Settings |
nav.cms |
/cms |
Owner | FileText |
nav.profile |
/profile |
all | User |
New Components
MobileBar.tsx
Path: frontend/src/components/layout/MobileBar.tsx
Slim bar, visible only on < md. Contains hamburger button (Menu icon) and app name.
<header className="flex h-14 items-center gap-3 border-b border-border bg-card px-4 md:hidden"
data-testid="app-mobile-bar">
<button onClick={onMenuOpen} aria-label="Open navigation" data-testid="mobile-menu-button">
<Menu />
</button>
<span className="font-semibold">{t('common.appName')}</span>
</header>
Props: onMenuOpen: () => void
SidebarOverlay.tsx
Path: frontend/src/components/layout/SidebarOverlay.tsx
Wraps <Sidebar onClose={...} /> in a backdrop overlay for mobile:
<div className="fixed inset-0 z-40 md:hidden">
<div className="absolute inset-0 bg-black/50" onClick={onClose} data-testid="sidebar-backdrop" />
<div className="absolute left-0 top-0 h-full w-64 shadow-xl">
<Sidebar onClose={onClose} />
</div>
</div>
Only rendered when mobile menu is open.
ThemeToggle.tsx
Path: frontend/src/components/layout/ThemeToggle.tsx
Button that reads/writes cms-theme in localStorage and toggles dark class on <html>.
<button
onClick={toggleTheme}
aria-label={isDark ? t('theme.switchToLight') : t('theme.switchToDark')}
data-testid="theme-toggle"
>
{isDark ? <Sun /> : <Moon />}
</button>
Uses useTheme() hook (see below).
useTheme.ts
Path: frontend/src/hooks/useTheme.ts
function useTheme(): { theme: Theme; toggleTheme: () => void }
- Reads initial value from
localStorage['cms-theme']orprefers-color-scheme - On toggle: flips theme, writes to localStorage, applies class to
document.documentElement
Removed Components
Topbar.tsx
Deleted. Functionality moved to Sidebar footer (LanguageSwitcher, UserMenu).
data-testid="app-topbar" removed from the DOM.
i18n Keys (additions to translation.json)
{
"nav": {
"settings": "Instellingen"
},
"theme": {
"switchToLight": "Overschakelen naar licht thema",
"switchToDark": "Overschakelen naar donker thema"
}
}
(English equivalents added to en/translation.json)
Test IDs Summary
| Element | data-testid |
|---|---|
| Sidebar (desktop) | app-sidebar |
| Mobile bar | app-mobile-bar |
| Hamburger button | mobile-menu-button |
| Sidebar backdrop | sidebar-backdrop |
| Theme toggle button | theme-toggle |
| Nav: dashboard | nav-dashboard |
| Nav: users | nav-users |
| Nav: settings | nav-settings |
| Nav: cms | nav-cms |
| Nav: profile | nav-profile |
| Main content area | app-main |