diff --git a/aidlc-docs/features/cms-frontend/construction/plans/unit-3-functional-design-plan.md b/aidlc-docs/features/cms-frontend/construction/plans/unit-3-functional-design-plan.md index 487dda4..ffbec82 100644 --- a/aidlc-docs/features/cms-frontend/construction/plans/unit-3-functional-design-plan.md +++ b/aidlc-docs/features/cms-frontend/construction/plans/unit-3-functional-design-plan.md @@ -1,6 +1,6 @@ # Functional Design Plan — Unit 3: Layout & Navigation -**Status**: 📋 Awaiting answers +**Status**: ✅ Complete ## Unit Context - **Unit**: Unit 3 — Layout & Navigation @@ -19,7 +19,7 @@ A) Remove the Topbar entirely — move LanguageSwitcher and UserMenu to the side 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 -[Answer]: +[Answer]: A --- @@ -30,7 +30,7 @@ A) Slide-over overlay — hamburger button in a slim top bar opens the full side 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]: +[Answer]: A --- @@ -41,7 +41,7 @@ A) Sidebar footer — next to LanguageSwitcher and UserMenu (recommended — kee B) Top of the sidebar — visible without scrolling C) Only accessible from the Profile page (not in the sidebar) -[Answer]: +[Answer]: A --- @@ -52,7 +52,7 @@ A) Inline script in `index.html` — runs synchronously before React loads (reco 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]: +[Answer]: A --- @@ -71,4 +71,4 @@ A) Filter sidebar items client-side based on `user.role` from AuthContext (recom 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]: +[Answer]: A diff --git a/aidlc-docs/features/cms-frontend/construction/unit-3/functional-design/business-logic-model.md b/aidlc-docs/features/cms-frontend/construction/unit-3/functional-design/business-logic-model.md new file mode 100644 index 0000000..691ed90 --- /dev/null +++ b/aidlc-docs/features/cms-frontend/construction/unit-3/functional-design/business-logic-model.md @@ -0,0 +1,74 @@ +# Business Logic Model — Unit 3: Layout & Navigation + +## Component Hierarchy + +``` +AppLayout +├── Sidebar desktop: always visible (md+) +│ ├── SidebarHeader (logo + app name) +│ ├── NavList +│ │ └── NavItem × N filtered by user.role +│ └── SidebarFooter +│ ├── LanguageSwitcher moved from Topbar +│ ├── ThemeToggle new +│ └── UserMenu moved from Topbar +├── MobileBar mobile only (< md) +│ └── hamburger button → opens SidebarOverlay +├── SidebarOverlay rendered only when mobile menu is open +│ ├── backdrop (closes on click) +│ └── Sidebar (with onClose prop) +└──
+ └── +``` + +## NavItem Filtering + +```ts +const visibleItems = NAV_ITEMS.filter(item => + !item.roles || item.roles.includes(user.role) +); +``` + +`item.roles === undefined` means visible to all authenticated users. Evaluated at render time. + +## Mobile Overlay State + +`isMenuOpen: boolean` state lives in `AppLayout`. Passed as: +- `onMenuOpen` to `MobileBar` +- `onClose` to `SidebarOverlay` → forwarded as `onClose` to `Sidebar` + +Closes automatically on route change via `useEffect` watching the current pathname. + +## Theme Initialisation (no flash) + +A blocking inline ` +``` + +The `useTheme` hook reads from `localStorage` on mount and keeps toggle in sync with the DOM class. + +## Theme Toggle Logic (`useTheme`) + +```ts +function toggleTheme() { + const next = theme === 'dark' ? 'light' : 'dark'; + setTheme(next); + localStorage.setItem('cms-theme', next); + document.documentElement.classList.remove('light', 'dark'); + document.documentElement.classList.add(next); +} +``` + +## Logout Flow (unchanged) + +`UserMenu` in sidebar footer calls `useAuth().logout()` then `navigate({ to: '/login' })`. diff --git a/aidlc-docs/features/cms-frontend/construction/unit-3/functional-design/business-rules.md b/aidlc-docs/features/cms-frontend/construction/unit-3/functional-design/business-rules.md new file mode 100644 index 0000000..e5958ef --- /dev/null +++ b/aidlc-docs/features/cms-frontend/construction/unit-3/functional-design/business-rules.md @@ -0,0 +1,56 @@ +# Business Rules — Unit 3: Layout & Navigation + +## Navigation Visibility Rules (BR-U3-01 – 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 via TanStack Router `activeProps`. Exact match for leaf routes; prefix match for section roots. + +--- + +## Responsive Behaviour (BR-U3-08) + +- **≥ 768px (md)**: sidebar always visible at fixed width (`w-64`), no top bar. +- **< 768px**: sidebar hidden by default. A slim mobile bar (hamburger + app name) is shown. Tapping the hamburger opens the sidebar as a slide-over overlay. +- The overlay closes when: the user taps the backdrop, taps the close button in the sidebar, or navigates to a new route. + +--- + +## Topbar Removal (BR-U3-09) + +The `Topbar` component is removed entirely. No top bar exists on desktop. A slim mobile-only bar replaces it solely for the hamburger button. All functionality previously in the Topbar (`LanguageSwitcher`, `UserMenu`) moves to the sidebar footer. + +--- + +## Theme Toggle (BR-U3-10 – BR-U3-13) + +| Rule | Description | +|---|---| +| BR-U3-10 | Toggle switches between `light` and `dark` class on `` immediately | +| BR-U3-11 | Chosen theme persisted in `localStorage` under key `cms-theme` | +| BR-U3-12 | On app load, persisted preference applied before first React render via inline script in `index.html` (no flash) | +| BR-U3-13 | If no preference is stored, OS preference (`prefers-color-scheme`) is used as default | + +Only `cms-theme` is stored in localStorage — no auth data. + +--- + +## Sidebar Footer (BR-U3-14) + +The sidebar footer contains three controls in a fixed bottom section: +1. `LanguageSwitcher` (moved from Topbar) +2. `ThemeToggle` (new) +3. `UserMenu` (moved from Topbar) — shows name, email, logout action diff --git a/aidlc-docs/features/cms-frontend/construction/unit-3/functional-design/domain-entities.md b/aidlc-docs/features/cms-frontend/construction/unit-3/functional-design/domain-entities.md new file mode 100644 index 0000000..5ab0b0a --- /dev/null +++ b/aidlc-docs/features/cms-frontend/construction/unit-3/functional-design/domain-entities.md @@ -0,0 +1,37 @@ +# 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[] \| undefined` | Roles that can see this item; `undefined` = all authenticated roles | + +## Role (from AuthContext — existing) + +```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 on ``. + +## SupportedLanguage (existing, Unit 1) + +```ts +type SupportedLanguage = 'en' | 'nl'; +``` + +No changes — `changeLanguage` from `@/i18n/config` is reused as-is. diff --git a/aidlc-docs/features/cms-frontend/construction/unit-3/functional-design/frontend-components.md b/aidlc-docs/features/cms-frontend/construction/unit-3/functional-design/frontend-components.md new file mode 100644 index 0000000..ba227bb --- /dev/null +++ b/aidlc-docs/features/cms-frontend/construction/unit-3/functional-design/frontend-components.md @@ -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 +
+ + {isMenuOpen && setIsMenuOpen(false)} />} +
+ setIsMenuOpen(true)} /> +
+ +
+
+
+``` + +--- + +### `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 +
+ + {t('common.appName')} +
+``` + +--- + +### `SidebarOverlay.tsx` +**Path**: `frontend/src/components/layout/SidebarOverlay.tsx` + +Full-screen overlay wrapping `Sidebar` for mobile. Backdrop closes it on click. + +```tsx +
+
+
+ +
+
+``` + +Props: `onClose: () => void` + +--- + +### `ThemeToggle.tsx` +**Path**: `frontend/src/components/layout/ThemeToggle.tsx` + +Toggles dark/light mode. Uses `useTheme()` hook. + +```tsx + +``` + +--- + +### `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 ``. + +--- + +## Removed Components + +### `Topbar.tsx` +Deleted. `data-testid="app-topbar"` removed from the DOM. + +--- + +## `index.html` Change + +Add no-flash inline script in `` before any stylesheet: + +```html + +``` + +--- + +## 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` |