Parcourir la source

refactor(docs): update frontend UI documentation for navigation polish and session handling improvements

Code_Uwe il y a 1 semaine
Parent
commit
72e5e3f552
1 fichiers modifiés avec 188 ajouts et 305 suppressions
  1. 188 305
      Docs/frontend-ui.md

+ 188 - 305
Docs/frontend-ui.md

@@ -1,14 +1,4 @@
-<!-- --------------------------------------------------------------------------- -->
-
-<!-- Folder: docs -->
-
-<!-- File: frontend-ui.md -->
-
-<!-- Relative Path: docs/frontend-ui.md -->
-
-<!-- --------------------------------------------------------------------------- -->
-
-# Frontend UI: App Shell, Routing, Auth/RBAC, Explorer, Search, and Date Range Filter (RHL-019 / RHL-020 / RHL-021 / RHL-022 / RHL-023 / RHL-024 / RHL-025 / RHL-037)
+# Frontend UI: App Shell, Routing, Auth/RBAC, Explorer, Search, Date Range Filter, and Navigation Polish (RHL-019 / RHL-020 / RHL-021 / RHL-022 / RHL-023 / RHL-024 / RHL-025 / RHL-037 / RHL-032)
 
 This document describes the **frontend routing scaffold**, the **application shell layout**, and the core UI modules for the RHL Lieferscheine app.
 
@@ -22,10 +12,11 @@ Timeline:
 - **RHL-024**: Search UI v1 (URL-driven `q`, scopes, cursor pagination, open PDF + jump to day).
 - **RHL-025**: Search date range filter (from/to) with a calendar popover, presets, local validation, and URL sync.
 - **RHL-037**: Search scope UX improvements (TopNav deep-link branch switching + Single combobox + Multi selection UX + deterministic URL state).
+- **RHL-032**: Navigation UX polish (TopNav branding, theme toggle, user menu, tooltips, session indicator without content flicker, favicon, active states, and debounced loading UI).
 
 > **Language policy**
 >
-> - Conversation and developer coordination can be German.
+> - Developer coordination can be German.
 > - **Code, comments, tests, and documentation are English.**
 > - **All user-facing UI strings are German** (labels, button text, alerts, hints).
 
@@ -33,12 +24,12 @@ Timeline:
 
 ## 1. Scope
 
-### 1.1 Implemented (as of RHL-037 + RHL-025)
+### 1.1 Implemented (as of RHL-037 + RHL-025 + RHL-032)
 
 - **Public** `/login` route with a functional login form (shadcn/ui primitives).
 
 - **Protected** application shell for all authenticated routes:
-  - Top navigation (brand, status, logout)
+  - Top navigation (branding, quick navigation, actions)
   - Sidebar placeholder area
   - Main content area
 
@@ -51,7 +42,7 @@ Timeline:
   - AppShell (TopNav + sidebar) remains stable; no “blank spinner screens”.
 
 - **Logout**:
-  - Logout button calls `GET /api/auth/logout` and redirects to `/login?reason=logged-out`.
+  - Logout action calls `GET /api/auth/logout` and redirects to `/login?reason=logged-out`.
 
 - **UI RBAC (branch-level)**:
   - `BranchGuard` prevents branch users from accessing other branches’ URLs.
@@ -89,41 +80,24 @@ Timeline:
 
 - **Search UI (RHL-024) + Scope UX improvements (RHL-037)**
   - Route: `/:branch/search` (protected).
-
-  - URL-driven state for shareability:
-    - `q` (search query)
-
-    - scope semantics:
-      - **Single**: route branch is the scope (`/:branch/search`). No `branch=` query parameter is required.
-      - **Multi**: `scope=multi&branches=NL06,NL20` (deterministic order, unique list)
-      - **All**: `scope=all`
-
-    - `limit` (optional): `50 | 100 | 200` (default 100)
-
-    - `from` / `to` (optional): ISO date range filter (`YYYY-MM-DD`) synced to the URL (RHL-025)
-
+  - URL-driven state for shareability: `q`, `scope`, `branches`, `limit`, `from`, `to`.
   - Cursor-based pagination (`nextCursor`) is **not stored in the URL**.
 
-  - Admin/dev UX:
-    - **TopNav branch switch navigates** (deep-link branch switching):
-      - preserves the current deep path (Explorer and Search)
-      - preserves shareable Search query params (`q`, `scope`, `branches`, `limit`, `from`, `to`)
-      - keeps Single scope consistent with the route branch.
-
-    - **Single scope branch selection** uses a shadcn combobox to switch the _route branch_.
-
-    - **Multi scope branch selection** uses a checkbox grid (optimized for large branch counts) and provides an **“Alle abwählen”** action.
-
-  - **Search date range filter (RHL-025)**
-    - A calendar popover allows selecting `from` and `to`.
-    - Presets (“Heute”, “Letzte 7 Tage”, …) set common ranges quickly.
-    - The active filter is shown as a compact chip and can be cleared.
-    - The filter is validated locally (fast feedback) and is also validated by the backend.
-
-    Important UX policy (current UI v1):
-    - The Search UI still triggers queries only when a non-empty `q` is present.
-    - `from` / `to` are treated as **additive filters** for a text query.
-    - The backend supports date-only searches, but the current UI intentionally avoids this to prevent accidental broad queries.
+- **Search date range filter (RHL-025)**
+  - A calendar popover allows selecting `from` and `to`.
+  - Presets (“Heute”, “Letzte 7 Tage”, …) set common ranges quickly.
+  - The active filter is shown as a compact chip and can be cleared.
+  - The filter is validated locally (fast feedback) and is also validated by the backend.
+
+- **TopNav / navigation polish (RHL-032)**
+  - Solid header (no blur/transparency) for crisp borders.
+  - Branding logo (light + dark assets).
+  - Icon-only theme toggle.
+  - User dropdown menu (Profile / Support / Logout).
+  - Consistent tooltips across navigation.
+  - Session check indicator in TopNav (debounced) to avoid content flicker.
+  - Clear active states for Explorer and Search.
+  - Safe handling of invalid branch routes (admin/dev): warning + one-click recovery.
 
 ---
 
@@ -138,10 +112,6 @@ Timeline:
   - “Herunterladen” action (download variant) next to “Öffnen”
   - a dedicated in-app PDF viewer UI (instead of a new tab)
 
-- Perceived performance polish:
-  - client-side caching/prefetch
-  - skeleton/layout shift reduction
-
 ---
 
 ## 2. Route Groups & URL Structure
@@ -190,6 +160,7 @@ Responsibilities:
 - Global CSS imports (`app/globals.css`).
 - Theme provider setup (shadcn/ui + next-themes wrapper).
 - Base HTML/body structure.
+- **Icons / favicon** (RHL-032): metadata icons can be configured in the root layout.
 
 ### 3.2 Public layout
 
@@ -213,8 +184,8 @@ Responsibilities:
 
 UX rationale:
 
-- We keep the AppShell frame visible while auth/session checks run.
-- This avoids full-screen “blank spinners” on slow connections.
+- Keep the AppShell frame visible while auth/session checks run.
+- Avoid full-screen “blank spinners” on slow connections.
 
 ---
 
@@ -245,64 +216,136 @@ Below `2xl`:
 - Use full width for usability.
 - Hide the sidebar placeholder to avoid shrinking the main content area.
 
-### 4.2 Quick navigation (quality-of-life)
+### 4.2 TopNav structure (RHL-032)
+
+File: `components/app-shell/TopNav.jsx`
+
+TopNav is a sticky header and is the primary navigation surface.
+
+Layout groups (left → right):
+
+- **Brand**: logo link to `/` (light + dark variants).
+- **Primary navigation**: QuickNav (branch selector + Explorer/Search buttons).
+- **Actions**: theme toggle + session indicator.
+- **User menu**: dropdown trigger (Profile / Support / Logout).
+
+Design note:
+
+- The header is intentionally rendered as a **solid** background (no backdrop blur) to keep borders/buttons crisp.
+
+### 4.3 Branding (logo assets)
+
+Asset convention:
+
+- Store brand assets under `public/brand/`.
+- Use two assets when needed (light/dark) and toggle them via Tailwind `dark:` classes.
+
+### 4.4 Theme toggle
+
+File: `components/app-shell/ThemeToggleButton.jsx`
+
+- Icon-only toggle (sun/moon) using `next-themes`.
+- Preference respects system theme by default.
+
+### 4.5 Session indicator (no content flicker)
+
+File: `components/app-shell/SessionIndicator.jsx`
+
+Goal:
+
+- Avoid “session checking” cards flashing in the main content area.
+- Surface transient checks as a small indicator in the TopNav.
+
+Policy:
+
+- The indicator is **debounced** to avoid flicker on fast connections.
+- If the session check completes before the delay, nothing is shown.
+
+### 4.6 Tooltips policy (RHL-032)
+
+- Use shadcn/Radix tooltips for TopNav actions.
+- Prefer tooltips over native `title` attributes to avoid double tooltips.
+- A single `TooltipProvider` is mounted at TopNav scope so all triggers share the same delay configuration.
+
+### 4.7 User menu
+
+File: `components/app-shell/UserStatus.jsx`
+
+Menu items (German):
+
+- **Profil** → `/profile` (placeholder page until profile editing is implemented).
+- **Support** → opens a `mailto:` link to `info@attus.de`.
+- **Abmelden** → calls logout and redirects to login.
+
+Support mailto guidelines:
+
+- Build the `mailto:` query string with `encodeURIComponent` (not `URLSearchParams`) to avoid “+” rendering issues in some mail clients.
+- Include basic context in the mail body:
+  - current URL
+  - route path
+  - timestamp
+  - user role/branch
+  - user-agent
+
+---
+
+## 5. Quick navigation (quality-of-life)
 
 File: `components/app-shell/QuickNav.jsx`
 
 Purpose:
 
-- Reduce manual URL typing during manual testing and daily usage.
 - Provide direct links to:
   - Explorer (`/:branch`)
   - Search (`/:branch/search`)
 
+- For admin/dev: enable quick branch switching while preserving the current “context”.
+
 Behavior:
 
-- Branch users: QuickNav uses the user’s `branchId`.
+- Branch users:
+  - QuickNav is effectively fixed to their `branchId`.
 
 - Admin/dev users:
-  - QuickNav loads branches via `GET /api/branches`.
-  - Provides a branch dropdown.
-  - Stores the last selected branch in `localStorage` for convenience.
-
-Implementation notes:
+  - Loads branches via `GET /api/branches`.
+  - Stores the last selected branch in `localStorage` (`rhl_last_branch`) for convenience.
+  - Keeps `selectedBranch` stable and avoids update loops (guarded initialization).
 
-- Branch list loading is intentionally **not** tied to the dropdown selection.
-  - The list is fetched once for the authenticated admin/dev user (or when the user changes).
-  - This avoids unnecessary refetches when switching branches in the UI.
+Branch switching rule (RHL-037):
 
-RHL-037 navigation rule:
-
-- When admin/dev selects a branch in QuickNav, the app **navigates** to the same section while replacing the path branch segment:
-  - Explorer deep paths are preserved:
+- Selecting a branch navigates to the same section while replacing the first path segment.
+  - Explorer deep paths preserved:
     - `/NL32/2025/12/31` → `/NL20/2025/12/31`
 
-  - Search route is preserved and shareable query params are preserved:
-    - `/NL32/search?q=x&scope=multi&branches=NL06,NL20&limit=200&from=2026-01-01&to=2026-01-31`
-      → `/NL20/search?q=x&scope=multi&branches=NL06,NL20&limit=200&from=2026-01-01&to=2026-01-31`
-
-  - Single scope is kept consistent with the route branch (no divergence between UI selection and URL).
+  - Search route preserved and shareable params preserved:
+    - `/NL32/search?q=x&scope=multi&branches=NL06,NL20&limit=200&from=...&to=...`
+      → `/NL20/search?q=x&scope=multi&branches=NL06,NL20&limit=200&from=...&to=...`
 
-Implementation note:
+Implementation notes:
 
-- Helper logic for deep-path branch switching is centralized under `lib/frontend/quickNav/branchSwitch.js`.
+- Deep-path branch switching logic is centralized in `lib/frontend/quickNav/branchSwitch.js`.
+- Avoid using `useSearchParams()` inside QuickNav for “current query string” access.
+  - Use `window.location.search` at click-time instead (client-only).
+  - This avoids build-time issues for static/prerender contexts.
 
-Responsive behavior:
+Invalid branch routes (admin/dev) (RHL-032):
 
-- QuickNav is hidden on small screens by default (`md` and up only) to keep the header compact.
+- If the user manually navigates to a syntactically valid but non-existent branch (e.g. `/NL200`):
+  - QuickNav shows a warning indicator.
+  - The dropdown shows a warning explanation.
+  - A one-click recovery item is available: “Zur letzten gültigen Niederlassung”.
 
 ---
 
-## 5. Authentication UX (RHL-020)
+## 6. Authentication UX (RHL-020)
 
-### 5.1 Session check for protected routes
+### 6.1 Session check for protected routes
 
 File: `components/auth/AuthProvider.jsx`
 
 Behavior:
 
 1. On mount, call `apiClient.getMe()`.
-
 2. If `{ user: { ... } }`:
    - set auth state to `authenticated`
    - render protected UI
@@ -315,17 +358,21 @@ The `next` parameter:
 - includes the original `pathname` and query string
 - is sanitized to avoid open redirects (only internal paths are allowed)
 
-### 5.2 AuthGate (in-shell gating)
+### 6.2 AuthGate (in-shell gating)
 
 File: `components/auth/AuthGate.jsx`
 
 Behavior:
 
-- While session is loading: show an in-shell loading card.
+- While session is loading: render a minimal in-shell state.
 - On auth errors: show an in-shell error card + retry.
-- On unauthenticated: show an in-shell “redirecting” message while redirect happens.
+- On unauthenticated: show a short “redirecting” message while redirect happens.
+
+RHL-032 note:
 
-### 5.3 Login page (reason / next)
+- Prefer surfacing transient “session checking” feedback in the TopNav (SessionIndicator) to avoid content flicker.
+
+### 6.3 Login page (reason / next)
 
 Files:
 
@@ -335,13 +382,11 @@ Files:
 Flow:
 
 1. Login page parses query params using `parseLoginParams(...)`.
-
 2. If `reason` is present:
    - `expired` → show “session expired” banner (German)
    - `logged-out` → show “logged out” banner (German)
 
 3. On submit, the form calls `apiClient.login({ username, password })`.
-
 4. On success:
    - redirect to `next` if present
    - otherwise redirect to `/`
@@ -356,20 +401,11 @@ Username policy:
   - username input is normalized to lowercase
   - `autoCapitalize="none"` to prevent mobile auto-caps
 
-### 5.4 Logout
-
-File: `components/auth/LogoutButton.jsx`
-
-Flow:
-
-1. Calls `apiClient.logout()`.
-2. Redirects to `/login?reason=logged-out`.
-
 ---
 
-## 6. UI RBAC, Forbidden, and NotFound (RHL-021)
+## 7. UI RBAC, Forbidden, and NotFound (RHL-021)
 
-### 6.1 Goals
+### 7.1 Goals
 
 RHL-021 adds a friendly UI layer on top of backend RBAC:
 
@@ -382,7 +418,7 @@ Backend RBAC remains the source of truth. UI RBAC exists to:
 - prevent “obviously forbidden” navigation in the frontend
 - provide clear and consistent UX for end users
 
-### 6.2 BranchGuard (UI-side RBAC)
+### 7.2 BranchGuard (UI-side RBAC)
 
 Files:
 
@@ -405,7 +441,7 @@ Admin/dev branch existence validation:
   - If fetching the list fails, do not block rendering.
   - Backend RBAC and subsequent API calls remain authoritative.
 
-### 6.3 Param validation (year/month/day)
+### 7.3 Param validation (year/month/day)
 
 Files:
 
@@ -420,15 +456,15 @@ Layout enforcement (server-side `notFound()`):
 
 ---
 
-## 7. Explorer v2 (RHL-022) + PDF Open (RHL-023)
+## 8. Explorer v2 (RHL-022) + PDF Open (RHL-023)
 
-### 7.1 UI goal
+### 8.1 UI goal
 
 Provide a simple “file explorer” drill-down:
 
 - Year → Month → Day → Files
 
-### 7.2 Explorer pages
+### 8.2 Explorer pages
 
 Routes and components:
 
@@ -437,14 +473,14 @@ Routes and components:
 - `/:branch/:year/:month` → `components/explorer/levels/DaysExplorer.jsx`
 - `/:branch/:year/:month/:day` → `components/explorer/levels/FilesExplorer.jsx`
 
-### 7.3 Data fetching strategy
+### 8.3 Data fetching strategy
 
 - All Explorer pages are **Client Components**.
 - All JSON API calls go through `lib/frontend/apiClient.js`.
 - A small hook provides consistent query state:
   - `lib/frontend/hooks/useExplorerQuery.js`
 
-### 7.4 Breadcrumbs (with dropdowns)
+### 8.4 Breadcrumbs (with dropdowns)
 
 Files:
 
@@ -457,7 +493,7 @@ Files:
   - `lib/frontend/explorer/formatters.js` (German month labels)
   - `lib/frontend/explorer/sorters.js`
 
-### 7.5 Files list (leaf route) and “Open PDF”
+### 8.5 Files list (leaf route) and “Open PDF”
 
 Leaf route:
 
@@ -480,13 +516,11 @@ Implementation notes:
 - URL construction is centralized in:
   - `lib/frontend/explorer/pdfUrl.js`
 
-- The PDF endpoint is binary (`application/pdf`). The UI uses navigation (`<a target="_blank">`) instead of `apiClient`.
-
 ---
 
-## 8. Search UI (RHL-024 / RHL-037) + Date Range Filter (RHL-025)
+## 9. Search UI (RHL-024 / RHL-037) + Date Range Filter (RHL-025)
 
-### 8.1 Route and state model
+### 9.1 Route and state model
 
 Route:
 
@@ -509,193 +543,51 @@ Shareable params (first page identity):
 
 - Scope params:
   - **Single**: no `scope` param required; route branch defines the branch.
-    - Example: `/NL01/search?q=reifen`
-
-  - **Multi**: `scope=multi&branches=NL06,NL20`
-    - branches list is deterministic:
-      - unique
-      - stable ordering
-
+  - **Multi**: `scope=multi&branches=NL06,NL20` (deterministic)
   - **All**: `scope=all`
 
 - `limit`:
   - allowed values: `50 | 100 | 200`
   - default: `100`
-  - only written to URL when non-default to keep URLs readable
 
 - `from/to` (RHL-025):
   - ISO date filter in `YYYY-MM-DD`
-  - both keys are optional
   - preserved in URLs and across branch switching
 
-Pagination:
-
-- `nextCursor` is kept in client state.
-- “Mehr laden” triggers a request with `cursor=nextCursor` and appends results.
-
-### 8.2 Roles and scope UI
+### 9.2 Debounced loading UI (RHL-032)
 
-Branch users:
+Goal:
 
-- No scope selector.
-- The UI always searches within the current route branch.
+- Avoid visible “skeleton flashes” for fast requests.
 
-Admin/dev users:
+Approach:
 
-- Scope selector:
-  - “Diese Niederlassung” (single: route branch)
-  - “Mehrere Niederlassungen” (multi)
-  - “Alle Niederlassungen” (all)
-
-- Single branch selection:
-  - shadcn combobox
-  - selecting a branch **navigates** to `/:branch/search` while preserving shareable query params (`q`, scope params, `limit`, `from/to`).
-
-- Multi branch selection:
-  - checkbox grid optimized for large branch counts
-  - “selectable card” label wrapper highlights checked items (border + background)
-  - pointer cursor + hover affordances improve discoverability
-  - “Alle abwählen” button to reset selection
-
-- Branch list:
-  - loaded via `GET /api/branches`
-  - fail-open UI behavior:
-    - if branch list fails, Search UI remains usable
-    - allow manual NLxx input as a fallback for selection
-
-### 8.3 Search form structure
+- Loading UI is shown only after a small delay.
+- This is implemented with `useDebouncedVisibility(...)` and centralized timing constants.
 
 Files:
 
-- `components/search/SearchPage.jsx`
-- `components/search/SearchForm.jsx`
-
-Form building blocks:
-
-- `components/search/form/SearchQueryBox.jsx`
-- `components/search/form/SearchScopeSelect.jsx`
-- `components/search/form/SearchSingleBranchCombobox.jsx`
-- `components/search/form/SearchMultiBranchPicker.jsx`
-- `components/search/form/SearchLimitSelect.jsx`
-- `components/search/form/SearchDateRangePicker.jsx` (RHL-025)
-- `components/search/form/SearchDateFilterChip.jsx` (RHL-025)
-
-Key UX rules:
-
-- `q` is the primary trigger for searches (explicit submit, no debounce).
-- Date range changes update the URL and are applied when a query is active.
-- Validation errors are shown **near the form**, not duplicated in the results area.
-
-### 8.4 Result list and actions
-
-Search results show at least:
-
-- Branch (for multi/all)
-- Date (German formatted)
-- Filename
-- Relative path
-- Optional snippet (if returned by the provider)
-
-Actions:
-
-- “Öffnen”
-  - uses `buildPdfUrl(...)`
-  - opens the binary endpoint in a new tab (`<a target="_blank" rel="noopener noreferrer">`)
-
-- “Zum Ordner”
-  - navigates to `/:branch/:year/:month/:day` using `dayPath(...)`
-
-Implementation note:
-
-- In the Search results table, the actions are intentionally kept in a fixed-width column.
-- Buttons render side-by-side on desktop; on very small widths they can wrap.
-
-Sorting:
-
-- “Relevanz” (backend order)
-- “Datum (neueste zuerst)”
-- “Niederlassung” (NLxx ascending; with stable tie-breakers by date and filename)
-
-Totals:
-
-- When the backend returns `total`, the UI shows “x von y Treffern geladen”.
-- If `total` is `null`, the UI falls back to showing only the loaded count.
-
-### 8.5 Date range filter (RHL-025)
+- Timing constants: `lib/frontend/ui/uxTimings.js`
+  - `LOADING_UI_DELAY_MS`
+  - `SESSION_INDICATOR_DELAY_MS`
+  - `SESSION_INDICATOR_MIN_VISIBLE_MS`
+  - `TOOLTIP_DELAY_MS`
 
-#### 8.5.1 Goals
+- Debounce hook: `lib/frontend/hooks/useDebouncedVisibility.js`
 
-- Allow narrowing down text searches by **inclusive** date filters.
-- Keep the date filter **shareable** via the URL (`from`, `to`).
-- Provide **fast feedback** (local validation) while also relying on backend validation.
+Applied to:
 
-#### 8.5.2 URL representation
-
-- `from` and `to` are ISO strings: `YYYY-MM-DD`.
-
-- Open ranges are supported:
-  - only `from` → “ab <date>”
-  - only `to` → “bis <date>”
-
-- A single-day search is represented as:
-  - `from === to`
-
-#### 8.5.3 UI controls
-
-- Entry point: “Zeitraum” button in the Search form.
-
-- Popover contents:
-  - Two read-only inputs (“Von”, “Bis”) with clear buttons
-  - Two-month calendar view
-  - Presets (German) for common ranges
-  - Reset action
-
-- Active filter display:
-  - A compact chip below the form controls
-  - Provides a one-click clear action
-
-#### 8.5.4 Validation and error mapping
-
-Single Source of Truth (frontend):
-
-- ISO parsing + range checks:
-  - `lib/frontend/search/dateRange.js`
-
-- Canonical date-range validator:
-  - `lib/frontend/search/searchDateValidation.js`
-
-Local validation:
-
-- While editing, the UI produces a local `ApiClientError` using:
-  - `lib/frontend/search/dateFilterValidation.js`
-
-- This error is mapped to German UI copy via:
-  - `lib/frontend/search/errorMapping.js`
-
-Rules:
-
-- `from > to` is invalid and produces `VALIDATION_SEARCH_RANGE`.
-- Invalid ISO strings produce `VALIDATION_SEARCH_DATE`.
-- `from === to` is valid (single day).
-
-Backend alignment:
-
-- The backend also validates `from/to` and returns the same error codes.
-- This keeps frontend and backend behavior consistent.
-
-#### 8.5.5 Calendar integration details
-
-- The calendar is loaded client-side (SSR disabled) to avoid hydration/runtime issues.
-- The hook normalizes day-click handler signatures defensively to avoid version drift.
-- When the range is invalid (`from > to`), the calendar still shows the interval but highlights it as invalid.
+- Explorer level loading skeletons
+- Search results loading skeletons
+- TopNav session indicator
 
 ---
 
-## 9. UI primitives (shadcn/ui)
+## 10. UI primitives (shadcn/ui)
 
 The Explorer + auth + search UI uses shadcn/ui primitives from `components/ui/*`.
 
-Required components for the current scope:
+Core primitives:
 
 - `card`
 - `input`
@@ -713,10 +605,16 @@ Additional primitives used for Search scope UX:
 - `command`
 - `badge`
 - `calendar` (react-day-picker wrapper)
+- `tooltip` (RHL-032)
+
+Radix integration note:
+
+- Radix triggers (`DropdownMenuTrigger`, `TooltipTrigger`, …) require the trigger element to support `ref` forwarding.
+- The project’s `components/ui/button.jsx` forwards refs to remain compatible with Radix `asChild` usage.
 
 ---
 
-## 10. File Naming Convention (.js vs .jsx)
+## 11. File Naming Convention (.js vs .jsx)
 
 To keep the project consistent:
 
@@ -732,9 +630,9 @@ To keep the project consistent:
 
 ---
 
-## 11. Tests
+## 12. Tests
 
-### 11.1 Unit tests
+### 12.1 Unit tests
 
 Core tests:
 
@@ -777,7 +675,7 @@ Component SSR smoke test:
 
 - `components/app-shell/AppShell.test.js`
 
-### 11.2 Running tests
+### 12.2 Running tests
 
 From the repo root:
 
@@ -793,9 +691,9 @@ npm run build
 
 ---
 
-## 12. Manual verification checklist
+## 13. Manual verification checklist
 
-### 12.1 Local (Docker)
+### 13.1 Local (Docker)
 
 Start:
 
@@ -808,15 +706,19 @@ Verify flows in the browser:
 - Open a protected route while logged out (e.g. `/NL01/2025/12`)
   - Expect redirect to `/login?reason=expired&next=/NL01/2025/12`
 
-- Invalid login
-  - Expect a German error message (e.g. “Benutzername oder Passwort ist falsch.”)
-
 - Valid login
   - Expect redirect into the protected route
 
 - Logout
   - Expect redirect to `/login?reason=logged-out`
 
+Navigation/TopNav checks (RHL-032):
+
+- Tooltips show consistently (no double native tooltips).
+- Theme toggle switches theme.
+- Branch switching updates the URL and keeps context (Explorer/Search).
+- Invalid branch route (`/NL200`) shows warning + recovery item.
+
 Explorer checks:
 
 - `/:branch` shows years
@@ -824,12 +726,6 @@ Explorer checks:
 - `/:branch/:year/:month` shows days
 - `/:branch/:year/:month/:day` shows files
 
-PDF open:
-
-- On `/:branch/:year/:month/:day`, click **“Öffnen”**
-  - Expected: opens the PDF in a new tab
-  - Expected: works for filenames with spaces and special characters (URL-encoding)
-
 Search checks:
 
 - `/NL01/search`
@@ -838,27 +734,16 @@ Search checks:
 
 - admin/dev:
   - scope switching (single/multi/all)
-  - single branch selection via combobox
   - multi selection via checkbox grid + “Alle abwählen”
   - limit switching (50/100/200)
+  - date range filter updates `from/to` in the URL
 
-- date range filter (RHL-025):
-  - open “Zeitraum” popover and pick a range
-  - verify `from/to` are written to the URL
-  - verify chip shows the German label and can clear the filter
-  - verify presets (“Heute”, “Letzte 7 Tage”, …) set the expected range
-  - invalid range:
-    - ensure UI displays a validation message
-    - ensure the calendar highlights the invalid interval
-
-- pagination:
-  - “Mehr laden” appends results when `nextCursor` exists
+Debounced loading UI (RHL-032):
 
-- TopNav QuickNav:
-  - switching branch updates the URL and keeps the current deep path
-  - switching branch on Search keeps shareable query params (including `from/to`)
+- With fast connections: skeleton flashes are minimized.
+- With throttling: skeletons appear after the delay and remain stable.
 
-### 12.2 Server
+### 13.2 Server
 
 Deploy and verify on the server URL.
 
@@ -869,17 +754,15 @@ Verify:
   - scopes
   - limit selection
   - date range filter + URL sync
-  - total count (“x von y geladen”)
   - open PDF / jump to day
   - TopNav branch switching keeps deep links
 
 ---
 
-## 13. Planned follow-ups
+## 14. Planned follow-ups
 
 - Optional Search UX improvements:
   - grouping results by date / branch
-  - query presets (beyond the date presets)
   - optional date-only search mode (if desired)
 
 - Optional Explorer improvements: