docs(unit-3): Revert to open questions in functional design plan

Functional design documents were written based on self-answered questions
instead of asking the user. Removed pre-written docs and restored the plan
with open questions for the user to answer.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 11:47:33 +02:00
co-authored by Claude Haiku 4.5
parent 31cc45d79b
commit 037332e52f
5 changed files with 59 additions and 332 deletions
@@ -1,6 +1,6 @@
# Functional Design Plan — Unit 3: Layout & Navigation
**Status**: ✅ Complete
**Status**: 📋 Awaiting answers
## Unit Context
- **Unit**: Unit 3 — Layout & Navigation
@@ -8,30 +8,67 @@
- **Depends on**: Unit 2 (AuthContext, RoleGuard, useAuth)
- **Stories Covered**: US-08 (layout shell), US-18 (role-filtered sidebar), US-19 (theme toggle)
## Design Decisions
---
### Q1: Topbar
Remove the Topbar entirely on desktop. A slim mobile-only bar replaces it.
**Decision**: Remove — all chrome moves into the sidebar.
Please answer the following questions by filling in the letter after each `[Answer]:` tag.
### Q2: Mobile navigation
Use a slide-over sidebar overlay triggered by a hamburger button in the mobile bar.
**Decision**: Sidebar overlay (not bottom nav, not icon-only collapse).
## Question 1: Topbar
The current Topbar contains only the LanguageSwitcher and UserMenu.
### Q3: Theme toggle placement
Sidebar footer (bottom of sidebar), visible on all screen sizes.
**Decision**: Sidebar footer.
A) Remove the Topbar entirely — move LanguageSwitcher and UserMenu to the sidebar footer (recommended — sidebar becomes the only chrome, full width for page content)
B) Keep the Topbar — move LanguageSwitcher and UserMenu to the sidebar footer, leave Topbar as an empty placeholder for future use
C) Keep the Topbar with its current content, only add the same items also to the sidebar
### Q4: Theme initialisation (no flash)
Blocking inline script in `index.html` applies theme class before React hydrates.
**Decision**: Inline script in `index.html`.
[Answer]:
### Q5: Language switcher placement
Move from Topbar to sidebar footer alongside theme toggle and UserMenu.
**Decision**: Sidebar footer.
---
## Artefacts Produced
- `unit-3/functional-design/business-rules.md` — BR-U3-01 through BR-U3-15
- `unit-3/functional-design/domain-entities.md` — NavItem, Role, Theme, SupportedLanguage
- `unit-3/functional-design/business-logic-model.md` — component hierarchy, theme init, nav filtering, mobile state
- `unit-3/functional-design/frontend-components.md` — all modified/new/removed components with testIds
## Question 2: Mobile navigation
On small screens (< 768px) the sidebar is currently hidden. How should navigation work on mobile?
A) Slide-over overlay — hamburger button in a slim top bar opens the full sidebar as an overlay (recommended)
B) Bottom navigation bar — fixed bar at the bottom of the screen with icons only
C) Icon-only sidebar — sidebar collapses to icon-only width on mobile, expands on hover/click
[Answer]:
---
## Question 3: Theme toggle (dark/light mode)
US-19 requires a theme toggle. Where should it live?
A) Sidebar footer — next to LanguageSwitcher and UserMenu (recommended — keeps all UI controls in one place)
B) Top of the sidebar — visible without scrolling
C) Only accessible from the Profile page (not in the sidebar)
[Answer]:
---
## Question 4: Theme — no-flash initialisation
To avoid a flash of the wrong theme on page load, the theme needs to be applied before React hydrates.
A) Inline script in `index.html` — runs synchronously before React loads (recommended — standard approach, zero dependencies)
B) CSS-only — use `prefers-color-scheme` media query only, no localStorage persistence
C) Skip no-flash for now — accept a brief flash on load
[Answer]:
---
## Question 5: Role-filtered sidebar
The sidebar should only show nav items the current user is allowed to access (US-18). Items per role:
| Item | Owner | Admin | User |
|---|---|---|---|
| Dashboard | ✅ | ✅ | ✅ |
| User Management | ✅ | ✅ | ❌ |
| System Settings | ✅ | ❌ | ❌ |
| CMS Management | ✅ | ❌ | ❌ |
| Profile | ✅ | ✅ | ✅ |
A) Filter sidebar items client-side based on `user.role` from AuthContext (recommended — clean UX; backend remains authoritative for access control)
B) Show all items to all roles, grey out inaccessible ones
C) Show all items, let the route guard handle the redirect on click
[Answer]:
@@ -1,56 +0,0 @@
# 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:
```html
<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
```ts
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 `useEffect` watching `location.pathname`)
## Logout Flow (unchanged from Unit 1/2)
`UserMenu` in sidebar footer calls `useAuth().logout()` then `navigate({ to: '/login' })`.
@@ -1,65 +0,0 @@
# Business Rules — Unit 3: Layout & Navigation
## Navigation Visibility Rules (BR-U3-01 through BR-U3-06)
| Route / Item | Owner | Administrator | User | Rule ID |
|---|---|---|---|---|
| Dashboard (`/dashboard`) | ✅ visible | ✅ visible | ✅ visible | BR-U3-01 |
| User Management (`/users`) | ✅ visible | ✅ visible | ❌ hidden | BR-U3-02 |
| System Settings (`/settings`) | ✅ visible | ❌ hidden | ❌ hidden | BR-U3-03 |
| CMS Management (`/cms`) | ✅ visible | ❌ hidden | ❌ hidden | BR-U3-04 |
| Profile (`/profile`) | ✅ visible | ✅ visible | ✅ visible | BR-U3-05 |
| Logout | ✅ visible | ✅ visible | ✅ visible | BR-U3-06 |
Sidebar items are filtered client-side based on `user.role` from `AuthContext`. The backend is the authoritative enforcement point; sidebar filtering is defence-in-depth UX only.
---
## Active Route Highlighting (BR-U3-07)
- The sidebar highlights the nav item whose `to` path matches the current route (TanStack Router `activeProps`).
- Exact match for leaf routes (`/dashboard`, `/profile`); prefix match for section roots (`/users`, `/cms`, `/settings`).
---
## Responsive Behaviour (BR-U3-08)
- On screens **≥ 768px (md)**: sidebar is always visible at fixed width (256px / `w-64`).
- On screens **< 768px**: sidebar is hidden by default; a hamburger button in a slim top bar opens it as a slide-over overlay.
- The mobile overlay closes when: the user taps outside it, taps the close button, or navigates to a new route.
---
## Theme Toggle (BR-U3-09 through BR-U3-12)
| Rule | Description |
|---|---|
| BR-U3-09 | Toggle switches between `light` and `dark` class on `<html>` immediately |
| BR-U3-10 | Chosen theme is persisted in `localStorage` under key `cms-theme` |
| BR-U3-11 | On app load, persisted preference is applied before first render (no flash of unstyled content) |
| BR-U3-12 | If no preference is stored, OS preference (`prefers-color-scheme`) is used as default |
Only `cms-theme` is stored in localStorage — no auth data.
---
## Language Switcher (BR-U3-13)
- The language switcher (`LanguageSwitcher`) is moved from the Topbar into the sidebar footer.
- Behaviour (NL/EN toggle, i18next `changeLanguage`) is unchanged from Unit 1.
---
## Topbar Removal (BR-U3-14)
- The `Topbar` component is removed from `AppLayout`.
- A slim mobile-only bar replaces it (hamburger button + app name) — only rendered on `< md` screens.
- All functionality previously in Topbar (`LanguageSwitcher`, `UserMenu`) moves into the sidebar footer section.
---
## Layout Shell (BR-U3-15)
- `AppLayout` renders: `<Sidebar>` + `<main>` (full remaining width/height).
- No persistent top bar on desktop — the sidebar is the only chrome.
- `<main>` has padding (`p-6`) and fills remaining viewport height.
@@ -1,37 +0,0 @@
# Domain Entities — Unit 3: Layout & Navigation
## NavItem
Represents a single entry in the sidebar navigation. Defined as a constant array in `Sidebar.tsx`.
| Field | Type | Description |
|---|---|---|
| `to` | `string` | TanStack Router route path |
| `labelKey` | `string` | i18n key for the display label |
| `icon` | `LucideIcon` | Icon component from lucide-react |
| `testId` | `string` | `data-testid` value |
| `roles` | `Role[]` | Roles that can see this item (`undefined` = all roles) |
## Role (from AuthContext)
```ts
type Role = 'Owner' | 'Administrator' | 'User';
```
Sourced from `useAuth().user.role`. Used by sidebar to filter `NavItem[]`.
## Theme
```ts
type Theme = 'light' | 'dark';
```
Persisted in `localStorage` under key `cms-theme`. Applied as class `light` or `dark` on `<html>`.
## SupportedLanguage (existing, Unit 1)
```ts
type SupportedLanguage = 'en' | 'nl';
```
No changes from Unit 1 — `changeLanguage` from `@/i18n/config` is reused.
@@ -1,152 +0,0 @@
# 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:
```tsx
<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[]` to `NavItem` interface
- Filter `NAV_ITEMS` by `user.role` before rendering
- Add `SidebarFooter` section at bottom with `LanguageSwitcher`, `ThemeToggle`, `UserMenu`
- Accept optional `onClose?: () => void` prop 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.
```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="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:
```tsx
<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>`.
```tsx
<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`
```ts
function useTheme(): { theme: Theme; toggleTheme: () => void }
```
- Reads initial value from `localStorage['cms-theme']` or `prefers-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`)
```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` |