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:
+59
-22
@@ -1,6 +1,6 @@
|
|||||||
# Functional Design Plan — Unit 3: Layout & Navigation
|
# Functional Design Plan — Unit 3: Layout & Navigation
|
||||||
|
|
||||||
**Status**: ✅ Complete
|
**Status**: 📋 Awaiting answers
|
||||||
|
|
||||||
## Unit Context
|
## Unit Context
|
||||||
- **Unit**: Unit 3 — Layout & Navigation
|
- **Unit**: Unit 3 — Layout & Navigation
|
||||||
@@ -8,30 +8,67 @@
|
|||||||
- **Depends on**: Unit 2 (AuthContext, RoleGuard, useAuth)
|
- **Depends on**: Unit 2 (AuthContext, RoleGuard, useAuth)
|
||||||
- **Stories Covered**: US-08 (layout shell), US-18 (role-filtered sidebar), US-19 (theme toggle)
|
- **Stories Covered**: US-08 (layout shell), US-18 (role-filtered sidebar), US-19 (theme toggle)
|
||||||
|
|
||||||
## Design Decisions
|
---
|
||||||
|
|
||||||
### Q1: Topbar
|
Please answer the following questions by filling in the letter after each `[Answer]:` tag.
|
||||||
Remove the Topbar entirely on desktop. A slim mobile-only bar replaces it.
|
|
||||||
**Decision**: Remove — all chrome moves into the sidebar.
|
|
||||||
|
|
||||||
### Q2: Mobile navigation
|
## Question 1: Topbar
|
||||||
Use a slide-over sidebar overlay triggered by a hamburger button in the mobile bar.
|
The current Topbar contains only the LanguageSwitcher and UserMenu.
|
||||||
**Decision**: Sidebar overlay (not bottom nav, not icon-only collapse).
|
|
||||||
|
|
||||||
### Q3: Theme toggle placement
|
A) Remove the Topbar entirely — move LanguageSwitcher and UserMenu to the sidebar footer (recommended — sidebar becomes the only chrome, full width for page content)
|
||||||
Sidebar footer (bottom of sidebar), visible on all screen sizes.
|
B) Keep the Topbar — move LanguageSwitcher and UserMenu to the sidebar footer, leave Topbar as an empty placeholder for future use
|
||||||
**Decision**: Sidebar footer.
|
C) Keep the Topbar with its current content, only add the same items also to the sidebar
|
||||||
|
|
||||||
### Q4: Theme initialisation (no flash)
|
[Answer]:
|
||||||
Blocking inline script in `index.html` applies theme class before React hydrates.
|
|
||||||
**Decision**: Inline script in `index.html`.
|
|
||||||
|
|
||||||
### Q5: Language switcher placement
|
---
|
||||||
Move from Topbar to sidebar footer alongside theme toggle and UserMenu.
|
|
||||||
**Decision**: Sidebar footer.
|
|
||||||
|
|
||||||
## Artefacts Produced
|
## Question 2: Mobile navigation
|
||||||
- `unit-3/functional-design/business-rules.md` — BR-U3-01 through BR-U3-15
|
On small screens (< 768px) the sidebar is currently hidden. How should navigation work on mobile?
|
||||||
- `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
|
A) Slide-over overlay — hamburger button in a slim top bar opens the full sidebar as an overlay (recommended)
|
||||||
- `unit-3/functional-design/frontend-components.md` — all modified/new/removed components with testIds
|
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]:
|
||||||
|
|||||||
-56
@@ -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' })`.
|
|
||||||
-65
@@ -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.
|
|
||||||
-37
@@ -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.
|
|
||||||
-152
@@ -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` |
|
|
||||||
Reference in New Issue
Block a user