docs(unit-3): Complete functional design based on user answers
All Q1-Q5 answered A: - Topbar removed; LanguageSwitcher + UserMenu move to sidebar footer - Mobile: slide-over overlay with hamburger button - Theme toggle in sidebar footer - No-flash init via inline script in index.html - Role-filtered sidebar items client-side Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
+191
@@ -0,0 +1,191 @@
|
||||
# Frontend Components — Unit 3: Layout & Navigation
|
||||
|
||||
## Modified Components
|
||||
|
||||
### `AppLayout.tsx` (modify)
|
||||
**Path**: `frontend/src/components/layout/AppLayout.tsx`
|
||||
|
||||
Remove `Topbar`. Add `MobileBar` and `SidebarOverlay`. Manage `isMenuOpen` state.
|
||||
|
||||
```tsx
|
||||
<div className="flex min-h-svh">
|
||||
<Sidebar className="hidden md:flex" />
|
||||
{isMenuOpen && <SidebarOverlay onClose={() => setIsMenuOpen(false)} />}
|
||||
<div className="flex flex-1 flex-col">
|
||||
<MobileBar onMenuOpen={() => setIsMenuOpen(true)} />
|
||||
<main className="flex-1 p-6" data-testid="app-main">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `Sidebar.tsx` (modify)
|
||||
**Path**: `frontend/src/components/layout/Sidebar.tsx`
|
||||
|
||||
Changes:
|
||||
- Add `roles?: Role[]` to `NavItem` — filter visible items by `user.role`
|
||||
- Add `SidebarFooter` section at the bottom with `LanguageSwitcher`, `ThemeToggle`, `UserMenu`
|
||||
- Accept optional `onClose?: () => void` prop (used by mobile overlay close button)
|
||||
- Retain `data-testid="app-sidebar"`
|
||||
|
||||
Nav items and role visibility:
|
||||
|
||||
| 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`
|
||||
|
||||
Visible only on `< md`. Contains hamburger button and app name.
|
||||
|
||||
Props: `onMenuOpen: () => void`
|
||||
|
||||
```tsx
|
||||
<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={t('nav.openMenu')} data-testid="mobile-menu-button">
|
||||
<Menu className="size-5" />
|
||||
</button>
|
||||
<span className="font-semibold">{t('common.appName')}</span>
|
||||
</header>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `SidebarOverlay.tsx`
|
||||
**Path**: `frontend/src/components/layout/SidebarOverlay.tsx`
|
||||
|
||||
Full-screen overlay wrapping `Sidebar` for mobile. Backdrop closes it on click.
|
||||
|
||||
```tsx
|
||||
<div className="fixed inset-0 z-40 md:hidden" data-testid="sidebar-overlay">
|
||||
<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>
|
||||
```
|
||||
|
||||
Props: `onClose: () => void`
|
||||
|
||||
---
|
||||
|
||||
### `ThemeToggle.tsx`
|
||||
**Path**: `frontend/src/components/layout/ThemeToggle.tsx`
|
||||
|
||||
Toggles dark/light mode. Uses `useTheme()` hook.
|
||||
|
||||
```tsx
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
aria-label={isDark ? t('theme.switchToLight') : t('theme.switchToDark')}
|
||||
data-testid="theme-toggle"
|
||||
>
|
||||
{isDark ? <Sun className="size-4" /> : <Moon className="size-4" />}
|
||||
</button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `useTheme.ts`
|
||||
**Path**: `frontend/src/hooks/useTheme.ts`
|
||||
|
||||
```ts
|
||||
function useTheme(): { theme: Theme; isDark: boolean; toggleTheme: () => void }
|
||||
```
|
||||
|
||||
Reads initial value from `localStorage['cms-theme']` or `prefers-color-scheme`. Syncs DOM class on `<html>`.
|
||||
|
||||
---
|
||||
|
||||
## Removed Components
|
||||
|
||||
### `Topbar.tsx`
|
||||
Deleted. `data-testid="app-topbar"` removed from the DOM.
|
||||
|
||||
---
|
||||
|
||||
## `index.html` Change
|
||||
|
||||
Add no-flash inline script in `<head>` before any stylesheet:
|
||||
|
||||
```html
|
||||
<script>
|
||||
(function(){
|
||||
var s=localStorage.getItem('cms-theme');
|
||||
var t=s?s:(window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light');
|
||||
document.documentElement.classList.add(t);
|
||||
})();
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## i18n Additions
|
||||
|
||||
**`nl/translation.json`**:
|
||||
```json
|
||||
{
|
||||
"nav": {
|
||||
"settings": "Instellingen",
|
||||
"profile": "Profiel",
|
||||
"openMenu": "Navigatie openen"
|
||||
},
|
||||
"theme": {
|
||||
"switchToLight": "Overschakelen naar licht thema",
|
||||
"switchToDark": "Overschakelen naar donker thema"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**`en/translation.json`**:
|
||||
```json
|
||||
{
|
||||
"nav": {
|
||||
"settings": "Settings",
|
||||
"profile": "Profile",
|
||||
"openMenu": "Open navigation"
|
||||
},
|
||||
"theme": {
|
||||
"switchToLight": "Switch to light theme",
|
||||
"switchToDark": "Switch to dark theme"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Test IDs Summary
|
||||
|
||||
| Element | `data-testid` |
|
||||
|---|---|
|
||||
| Sidebar | `app-sidebar` |
|
||||
| Mobile bar | `app-mobile-bar` |
|
||||
| Hamburger button | `mobile-menu-button` |
|
||||
| Sidebar overlay container | `sidebar-overlay` |
|
||||
| Sidebar backdrop | `sidebar-backdrop` |
|
||||
| Theme toggle | `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` |
|
||||
Reference in New Issue
Block a user