mitgliederverwaltung/docs/umsetzung-sidebar.md
Simon 16ca4efc03
Some checks failed
continuous-integration/drone/push Build is failing
feat: implement standard-compliant sidebar with comprehensive tests
Implement a new sidebar component based on DaisyUI Drawer pattern without
custom CSS variants. The sidebar supports desktop (expanded/collapsed states)
and mobile (overlay drawer) with full accessibility compliance.

Sidebar Implementation:
- Refactor sidebar component with sidebar_header, menu_item, menu_group,
  sidebar_footer sub-components
- Add logo (mila.svg) with size-8 (32px) always visible
- Implement toggle button with icon swap (chevron-left/right) for desktop
- Add nested menu support with details/summary (expanded) and dropdown
  (collapsed) patterns
- Implement footer with language selector (expanded-only), theme toggle,
  and user menu with avatar
- Update layouts.ex to use drawer pattern with data-sidebar-expanded
  attribute for state management

CSS & JavaScript:
- Add CSS styles for sidebar state management via data-attribute selectors
- Implement SidebarState JavaScript hook for localStorage persistence
- Add smooth width transitions (w-64 ↔ w-16) for desktop collapsed state
- Add CSS classes for expanded-only, menu-label, and icon visibility

Documentation:
- Add sidebar-analysis-current-state.md: Analysis of current implementation
- Add sidebar-requirements-v2.md: Complete specification for new sidebar
- Add daisyui-drawer-pattern.md: DaisyUI pattern documentation
- Add umsetzung-sidebar.md: Step-by-step implementation guide

Testing:
- Add comprehensive component tests for all sidebar sub-components
- Add integration tests for sidebar state management and mobile drawer
- Extend accessibility tests (ARIA labels, roles, keyboard navigation)
- Add regression tests for duplicate IDs, hover effects, and tooltips
- Ensure full test coverage per specification requirements
2025-12-18 16:36:16 +01:00

1576 lines
39 KiB
Markdown

# Sidebar Neuimplementierung - Schritt-für-Schritt Anleitung
**Erstellt:** 2025-12-16
**Status:** Bereit zur Umsetzung
**Strategie:** Sequenzielle Tasks mit frischem Kontext pro Task
---
## Übersicht
Diese Anleitung zerlegt die komplexe Sidebar-Implementierung in 13 beherrschbare Subtasks. Jeder Task wird mit einem frischen Cursor-Agent im Auto-Mode (oder Sonnet 4.5 für komplexere Aufgaben) umgesetzt.
**Ziel:** Saubere, wartbare Sidebar-Implementierung nahe am DaisyUI-Standard ohne Custom-Variants oder Speziallösungen.
---
## Task 1: Vorbereitung & Analyse
**Agent:** Sonnet 4.5
**Geschätzte Dauer:** 10 Minuten
### Prompt:
```
Analysiere die aktuelle Sidebar-Implementierung in diesem Projekt und erstelle einen detaillierten Bericht:
1. Liste alle Dateien auf, die mit der Sidebar zusammenhängen
2. Dokumentiere die aktuelle Struktur (HTML, CSS, JavaScript)
3. Identifiziere alle Custom-CSS-Klassen und Variants
4. Dokumentiere die JavaScript-Hooks und ihre Funktionen
5. Erstelle eine Liste aller Dependencies (DaisyUI-Komponenten, Tailwind-Klassen)
Speichere den Bericht als `docs/sidebar-analysis-current-state.md`
Ziel: Vollständiges Verständnis des Ist-Zustands vor der Neuimplementierung.
```
### Acceptance Criteria:
- ✅ Alle Sidebar-bezogenen Dateien identifiziert
- ✅ Aktuelle Implementierung dokumentiert
- ✅ Custom CSS und JavaScript dokumentiert
- ✅ Bericht als Markdown gespeichert
---
## Task 2: DaisyUI Drawer Pattern Recherche
**Agent:** Sonnet 4.5 (wegen Web-Recherche)
**Geschätzte Dauer:** 15 Minuten
### Prompt:
```
Recherchiere und dokumentiere das Standard DaisyUI Drawer Pattern:
1. Lies die DaisyUI Dokumentation für die `drawer` Komponente
2. Finde Best-Practice-Beispiele für responsive Sidebars mit DaisyUI
3. Dokumentiere, wie drawer-toggle funktioniert
4. Dokumentiere, wie drawer-open für Desktop funktioniert
5. Erstelle Code-Beispiele für:
- Mobile Drawer (overlay)
- Desktop Sidebar (persistent)
- Kombination beider
Speichere die Dokumentation als `docs/daisyui-drawer-pattern.md`
Wichtig: Verwende KEINE custom CSS variants - nur Standard DaisyUI und Tailwind.
```
### Acceptance Criteria:
- ✅ DaisyUI Drawer Pattern dokumentiert
- ✅ Mobile und Desktop Patterns verstanden
- ✅ Code-Beispiele vorhanden
- ✅ Dokumentation gespeichert
---
## Task 3: Anforderungsdefinition & Design
**Agent:** Sonnet 4.5
**Geschätzte Dauer:** 20 Minuten
### Prompt:
```
Erstelle eine präzise Anforderungsspezifikation für die Sidebar basierend auf folgenden Anforderungen:
## Funktionale Anforderungen:
1. **Logo:**
- Immer sichtbar, gleiche Größe (32px / size-8)
- Sowohl im expanded als auch collapsed State
- Keine zwei verschiedenen Logo-Elemente
2. **Toggle-Button:**
- Nur auf Desktop sichtbar
- Icon-Swap: Chevron-left (expanded) ↔ Chevron-right (collapsed)
- Immer erreichbar
3. **Menü-Items:**
- Expanded: Icons + Text-Labels
- Collapsed: Nur Icons mit Tooltips (tooltip-right)
- Einheitlicher Hover-Effekt
4. **Nested Menu "Beiträge":**
- Expanded: Standard <details> mit <summary>
- Collapsed: DaisyUI dropdown dropdown-right
- Nur EIN Hover-Effekt, kein doppelter
5. **Footer:**
- IMMER am unteren Ende der Sidebar (via Flexbox)
- Theme-Toggle (immer sichtbar)
- Language-Selector (nur expanded)
- User-Menu mit Avatar (dropdown-top dropdown-end)
- Avatar: Erste Buchstabe, zentriert
6. **State Persistence:**
- localStorage: 'sidebar-expanded'
- data-attribute: [data-sidebar-expanded="true"|"false"]
7. **Responsive:**
- Mobile: Standard DaisyUI Drawer Overlay
- Desktop: Fixed Sidebar mit smooth width transition
## Aufgaben:
1. Erstelle Wireframes (als ASCII-Art) für:
- Desktop Expanded
- Desktop Collapsed
- Mobile mit Overlay
2. Liste alle benötigten DaisyUI-Komponenten auf
3. Definiere CSS-Strategie:
- Nur Tailwind + DaisyUI
- KEINE custom variants (@custom-variant)
- State-Management via data-attribute selectors
4. Definiere State-Management-Strategie:
- JavaScript Hook
- localStorage
- CSS reactions
Speichere als `docs/sidebar-requirements-v2.md`
```
### Acceptance Criteria:
- ✅ Anforderungen klar dokumentiert
- ✅ Wireframes erstellt
- ✅ Komponenten-Liste vorhanden
- ✅ CSS-Strategie definiert
- ✅ State-Management definiert
---
## Task 4: CSS Foundation
**Agent:** Auto-Mode
**Geschätzte Dauer:** 15 Minuten
### Prompt:
```
Erstelle die CSS-Grundlage für die neue Sidebar in `assets/css/app.css`:
## Schritt 1: Aufräumen
1. Entferne ALLE bestehenden Custom CSS Variants für Sidebar:
- @custom-variant is-drawer-open
- @custom-variant is-drawer-close
- Alle .is-drawer-* Regeln
2. Entferne alte Sidebar-spezifische Custom-Klassen
## Schritt 2: Neue CSS-Regeln erstellen
Erstelle CSS basierend auf `[data-sidebar-expanded]` Attribut:
```css
/* Desktop Sidebar Base */
.sidebar {
@apply flex flex-col bg-base-200 min-h-screen;
@apply transition-[width] duration-300 ease-in-out;
width: 16rem; /* Expanded: w-64 */
}
/* Collapsed State */
[data-sidebar-expanded="false"] .sidebar {
width: 4rem; /* Collapsed: w-16 */
}
/* Text Labels - Hide in Collapsed State */
.menu-label {
@apply transition-all duration-200 whitespace-nowrap;
}
[data-sidebar-expanded="false"] .sidebar .menu-label {
@apply opacity-0 w-0 overflow-hidden pointer-events-none;
}
/* Toggle Button Icon Swap */
.sidebar-collapsed-icon {
@apply hidden;
}
[data-sidebar-expanded="false"] .sidebar-expanded-icon {
@apply hidden;
}
[data-sidebar-expanded="false"] .sidebar-collapsed-icon {
@apply block;
}
/* Menu Groups - Show/Hide Based on State */
.expanded-menu-group {
@apply block;
}
.collapsed-menu-group {
@apply hidden;
}
[data-sidebar-expanded="false"] .sidebar .expanded-menu-group {
@apply hidden;
}
[data-sidebar-expanded="false"] .sidebar .collapsed-menu-group {
@apply block;
}
/* Elements Only Visible in Expanded State */
.expanded-only {
@apply block transition-opacity duration-200;
}
[data-sidebar-expanded="false"] .sidebar .expanded-only {
@apply hidden;
}
/* Tooltip - Only Show in Collapsed State */
.sidebar .tooltip::before,
.sidebar .tooltip::after {
@apply opacity-0 pointer-events-none;
}
[data-sidebar-expanded="false"] .sidebar .tooltip:hover::before,
[data-sidebar-expanded="false"] .sidebar .tooltip:hover::after {
@apply opacity-100;
}
/* Menu Item Alignment */
[data-sidebar-expanded="false"] .sidebar .menu > li > a,
[data-sidebar-expanded="false"] .sidebar .menu > li > button {
@apply justify-center px-0;
}
```
## Schritt 3: Testen
- Kompiliere CSS: `mix assets.build`
- Prüfe auf Fehler
- Stelle sicher, dass keine alten Custom-Variants mehr existieren
Verwende AUSSCHLIESSLICH:
- Tailwind @apply
- Standard DaisyUI Klassen
- CSS attribute selectors für state
```
### Acceptance Criteria:
- ✅ Alte Custom Variants entfernt
- ✅ Neue CSS-Regeln erstellt
- ✅ CSS kompiliert ohne Fehler
- ✅ Nur Standard Tailwind/DaisyUI verwendet
---
## Task 5: Layout-Struktur
**Agent:** Auto-Mode
**Geschätzte Dauer:** 20 Minuten
### Prompt:
```
Implementiere die grundlegende Layout-Struktur für die Sidebar in `lib/mv_web/components/layouts.ex`:
## Anforderungen:
1. Nutze DaisyUI `drawer` + `drawer-open` Pattern
2. Ein Container für Mobile UND Desktop (keine Duplikate!)
3. `@inner_block` darf nur EINMAL gerendert werden
4. `data-sidebar-expanded` Attribut auf root
5. `phx-hook="SidebarState"` auf root
6. `id="main-sidebar"` auf main content (für Tests)
## Implementierung:
Ersetze die bestehende `app/1` Funktion mit:
```heex
def app(assigns) do
club_name = get_club_name()
assigns = assign(assigns, :club_name, club_name)
~H"""
<%= if @current_user do %>
<div id="app-layout" class="drawer lg:drawer-open" data-sidebar-expanded="true" phx-hook="SidebarState">
<input id="mobile-drawer" type="checkbox" class="drawer-toggle" />
<div class="drawer-content flex flex-col">
<!-- Mobile Header (only visible on mobile) -->
<header class="lg:hidden sticky top-0 z-10 navbar bg-base-100 shadow-sm">
<label for="mobile-drawer" class="btn btn-square btn-ghost" aria-label={gettext("Open navigation menu")}>
<.icon name="hero-bars-3" class="size-6" aria-hidden="true" />
</label>
<span class="font-bold">{@club_name}</span>
</header>
<!-- Main Content (shared between mobile and desktop) -->
<main id="main-sidebar" class="px-4 py-8 sm:px-6 lg:px-8">
<div class="mx-auto space-y-4 max-full">
{render_slot(@inner_block)}
</div>
</main>
</div>
<div class="drawer-side z-20">
<label for="mobile-drawer" aria-label="close sidebar" class="drawer-overlay lg:hidden"></label>
<!-- Sidebar Content wird in nächstem Task implementiert -->
<aside class="sidebar">
<div class="p-4">
<p>Sidebar Placeholder</p>
</div>
</aside>
</div>
</div>
<% else %>
<!-- Not logged in -->
<main class="px-4 py-8 sm:px-6">
<div class="mx-auto space-y-4 max-full">
{render_slot(@inner_block)}
</div>
</main>
<% end %>
<.flash_group flash={@flash} />
"""
end
```
## Wichtig:
- @inner_block wird nur EINMAL gerendert (im drawer-content)
- Mobile und Desktop teilen sich den gleichen main-content
- Sidebar-Inhalt kommt in späteren Tasks
## Testen:
1. Kompiliere: `mix compile`
2. Starte Server: `mix phx.server`
3. Prüfe: Keine duplicate ID Fehler in Browser Console
4. Prüfe: Layout funktioniert responsive
5. Prüfe: Mobile Header erscheint nur auf Mobile
```
### Acceptance Criteria:
- ✅ Layout-Struktur implementiert
- ✅ Keine duplicate IDs
- ✅ @inner_block nur einmal gerendert
- ✅ Responsive funktioniert
- ✅ Kompiliert ohne Fehler
---
## Task 6: Sidebar Header Komponente
**Agent:** Auto-Mode
**Geschätzte Dauer:** 20 Minuten
### Prompt:
```
Implementiere die Sidebar-Header-Komponente in `lib/mv_web/components/layouts/sidebar.ex`:
## Anforderungen:
1. **Logo:**
- Immer `size-8` (32px)
- Immer sichtbar (kein Hide)
- Nur EIN Logo-Element
- Pfad: `/images/mila.svg`
2. **Club-Name:**
- Text-Label mit CSS-Klasse `menu-label`
- Wird via CSS ausgeblendet (collapsed)
- `text-lg font-bold truncate`
3. **Toggle-Button:**
- Nur auf Desktop sichtbar (responsive: `hidden lg:flex` oder `lg:block`)
- DaisyUI-konforme Button-Variante wählen:
* Option A: `btn btn-ghost btn-sm btn-square` (minimal, icon-only)
* Option B: `btn btn-ghost btn-sm` (mit etwas Padding)
* Option C: `btn btn-ghost btn-circle` (rund, falls besser zum Design passt)
- Icon-Strategie (wähle die beste Variante):
* Option A: Zwei Icons mit CSS-Klassen (`.sidebar-expanded-icon` / `.sidebar-collapsed-icon`)
* Option B: Ein Icon mit CSS transform (rotate bei collapsed)
* Option C: Ein Icon mit CSS content-swap (via ::before/::after)
- Event-Handler (wähle passende Variante):
* Option A: `onclick="toggleSidebar()"` (wenn global function vorhanden)
* Option B: `phx-click="toggle_sidebar"` (wenn LiveView event)
* Option C: `phx-hook="SidebarToggle"` (wenn Hook-basiert)
- ARIA: `aria-label={gettext("Toggle sidebar")}` und `aria-expanded` (wird via JS gesetzt)
## Design-Überlegungen:
- Button sollte sich harmonisch in den Header einfügen
- Position: Rechts im Header (`ml-auto`)
- Icon-Größe: `size-5` oder `size-4` (je nach Button-Größe)
- Icon-Typ: Chevron (left/right) oder Arrow (left/right) - wähle das passendere
- Hover-Effekt: Standard DaisyUI btn-ghost hover
## Implementierung:
Erstelle `sidebar_header/1` Funktion mit:
- Flexbox-Layout für Header (Logo + Name + Toggle)
- Responsive Toggle-Button
- Icon-Swap-Mechanismus (wähle beste Variante)
- Korrekte ARIA-Attribute
## Empfehlung:
Für DaisyUI-Konformität und Wartbarkeit:
- Button: `btn btn-ghost btn-sm btn-square` (icon-only, minimal)
- Icons: Zwei separate Icons mit CSS-Klassen (einfach, klar)
- Event: `onclick="toggleSidebar()"` (wenn JS Hook vorhanden) ODER `phx-click` (wenn LiveView)
## Beispiel-Struktur (als Orientierung):
```elixir
defp sidebar_header(assigns) do
~H"""
<div class="flex items-center gap-3 p-4 border-b border-base-300">
<!-- Logo -->
<img src={~p"/images/mila.svg"} alt="Mila Logo" class="size-8 shrink-0" />
<!-- Club Name -->
<span class="menu-label text-lg font-bold truncate">
{@club_name}
</span>
<!-- Toggle Button (Desktop only) -->
<%= unless @mobile do %>
<button
type="button"
id="sidebar-toggle"
class="hidden lg:flex ml-auto btn btn-ghost btn-sm btn-square"
aria-label={gettext("Toggle sidebar")}
onclick="toggleSidebar()"
>
<!-- Icon-Implementierung nach gewählter Strategie -->
</button>
<% end %>
</div>
"""
end
```
## Integration in layouts.ex:
Ersetze den Sidebar Placeholder in `layouts.ex`:
```heex
<aside class="sidebar">
<.sidebar_content current_user={@current_user} club_name={@club_name} mobile={false} />
</aside>
```
## Testen:
1. Kompiliere: `mix compile`
2. Starte Server: `mix phx.server`
3. Prüfe:
- Logo ist immer 32px groß
- Toggle-Button erscheint nur auf Desktop
- Button-Design passt zum Rest der Sidebar
- Icon wechselt beim Toggle
- Hover-Effekt funktioniert
- ARIA-Attribute korrekt
- Club-Name verschwindet beim Collapse (wenn toggleSidebar() funktioniert)
```
### Acceptance Criteria:
- ✅ sidebar_header Komponente erstellt
- ✅ Logo immer gleich groß
- ✅ Toggle-Button nur auf Desktop
- ✅ DaisyUI-konforme Button-Klassen verwendet
- ✅ Icon-Swap funktioniert (egal welche Variante gewählt wurde)
- ✅ Event-Handler funktioniert
- ✅ Design fügt sich harmonisch ein
- ✅ Keine Layout-Breaks
---
## Task 7: Sidebar Navigation - Flat Items
**Agent:** Auto-Mode
**Geschätzte Dauer:** 25 Minuten
### Prompt:
```
Implementiere einfache Menü-Items (flat, ohne Nesting) in `lib/mv_web/components/layouts/sidebar.ex`:
## Menü-Items:
- Members (`/members`)
- Users (`/users`)
- Custom Fields (`/custom_fields`)
- Settings (Placeholder)
## Anforderungen:
1. **Expanded State:**
- Icon + Text-Label
- Standard DaisyUI menu hover
2. **Collapsed State:**gf
- Nur Icon
- Tooltip erscheint rechts (`tooltip-right`)
- Tooltip-Text aus `data-tip`
3. **Hover-Effekt:**
- Einheitlich (Standard DaisyUI menu)
- Keine custom hover-styles
4. **Active State:**
- Highlight für current_path (optional)
## Implementierung:
Füge in `sidebar.ex` hinzu:
```elixir
defp sidebar_menu(assigns) do
~H"""
<ul class="menu flex-1 w-full p-2" role="menubar">
<.menu_item
href={~p"/members"}
icon="hero-users"
label={gettext("Members")}
/>
<.menu_item
href={~p"/users"}
icon="hero-user-circle"
label={gettext("Users")}
/>
<.menu_item
href={~p"/custom_fields"}
icon="hero-rectangle-group"
label={gettext("Custom Fields")}
/>
<.menu_item
href="#"
icon="hero-cog-6-tooth"
label={gettext("Settings")}
/>
</ul>
"""
end
attr :href, :string, required: true
attr :icon, :string, required: true
attr :label, :string, required: true
defp menu_item(assigns) do
~H"""
<li role="none">
<.link
navigate={@href}
class="flex items-center gap-3 tooltip tooltip-right"
data-tip={@label}
role="menuitem"
>
<.icon name={@icon} class="size-5 shrink-0" aria-hidden="true" />
<span class="menu-label">{@label}</span>
</.link>
</li>
"""
end
```
Ersetze in `sidebar_content`:
```heex
<!-- Navigation -->
<%= if @current_user do %>
<.sidebar_menu />
<% end %>
```
## Testen:
1. Kompiliere und starte Server
2. Prüfe Expanded State:
- Icons + Labels sichtbar
- Hover funktioniert
3. Toggle zu Collapsed:
- Nur Icons sichtbar
- Tooltips erscheinen bei Hover
- Tooltips zeigen richtigen Text
4. Prüfe:
- Einheitlicher Hover-Effekt
- Navigation funktioniert (Links klickbar)
```
### Acceptance Criteria:
- ✅ menu_item Komponente erstellt
- ✅ 4 Menü-Items implementiert
- ✅ Tooltips funktionieren (nur collapsed)
- ✅ Icons sichtbar in beiden States
- ✅ Hover-Effekt einheitlich
- ✅ Navigation funktioniert
---
## Task 8: Sidebar Navigation - Nested Menu
**Agent:** Sonnet 4.5 (komplexer)
**Geschätzte Dauer:** 30 Minuten
### Prompt:
```
Implementiere das verschachtelte "Beiträge"-Menü (Contributions) mit ZWEI verschiedenen Darstellungen je nach State.
## Problem:
Das Nested Menu muss sich anders verhalten als flat items:
- **Expanded:** Nutzt <details> mit <summary> für auf/zuklappbar
- **Collapsed:** Nutzt DaisyUI dropdown für Flyout rechts vom Icon
## Anforderungen:
1. **Nur EIN Hover-Effekt** (nicht doppelt)
2. **Flyout erscheint rechts** vom Icon im collapsed state
3. **Smooth transitions** zwischen states
4. **Submenu-Items:** Beitragsarten, Einstellungen
## Implementierung:
Füge in `sidebar.ex` hinzu:
```elixir
attr :icon, :string, required: true
attr :label, :string, required: true
slot :inner_block, required: true
defp menu_group(assigns) do
~H"""
<li role="none" class="menu-group">
<!-- Expanded Mode: Details/Summary -->
<details class="expanded-menu-group">
<summary class="flex items-center gap-3" role="menuitem" aria-haspopup="true">
<.icon name={@icon} class="size-5 shrink-0" aria-hidden="true" />
<span class="menu-label">{@label}</span>
</summary>
<ul role="menu" class="ml-4">
{render_slot(@inner_block)}
</ul>
</details>
<!-- Collapsed Mode: Dropdown -->
<div class="collapsed-menu-group dropdown dropdown-right">
<button
type="button"
tabindex="0"
class="flex items-center justify-center w-full p-2 rounded-lg hover:bg-base-300 tooltip tooltip-right"
data-tip={@label}
aria-haspopup="menu"
aria-label={@label}
>
<.icon name={@icon} class="size-5" aria-hidden="true" />
</button>
<ul
tabindex="0"
class="dropdown-content menu bg-base-100 rounded-box shadow-lg z-50 min-w-48 p-2"
role="menu"
>
<li class="menu-title">{@label}</li>
{render_slot(@inner_block)}
</ul>
</div>
</li>
"""
end
attr :href, :string, required: true
attr :label, :string, required: true
defp menu_subitem(assigns) do
~H"""
<li role="none">
<.link navigate={@href} role="menuitem">
{@label}
</.link>
</li>
"""
end
```
Füge in `sidebar_menu` nach den flat items hinzu:
```elixir
<!-- Nested Menu: Contributions -->
<.menu_group
icon="hero-currency-dollar"
label={gettext("Contributions")}
>
<.menu_subitem href="/contribution_types" label={gettext("Contribution Types")} />
<.menu_subitem href="/contribution_settings" label={gettext("Settings")} />
</.menu_group>
```
## CSS-Prüfung:
Stelle sicher, dass in `app.css` folgende Regeln existieren:
```css
/* Expanded: Show details, hide dropdown */
.expanded-menu-group {
@apply block;
}
.collapsed-menu-group {
@apply hidden;
}
/* Collapsed: Hide details, show dropdown */
[data-sidebar-expanded="false"] .sidebar .expanded-menu-group {
@apply hidden;
}
[data-sidebar-expanded="false"] .sidebar .collapsed-menu-group {
@apply block;
}
```
## Testen:
1. **Expanded State:**
- Details/Summary erscheint
- Klick öffnet/schließt Submenu
- Submenu-Items sind klickbar
- NUR EIN Hover-Effekt auf summary
2. **Collapsed State:**
- Dropdown erscheint
- Flyout öffnet RECHTS vom Icon
- Menu-Title "Contributions" erscheint
- Submenu-Items sind klickbar
- NUR EIN Hover-Effekt auf button
3. **Toggle zwischen States:**
- Smooth Transition
- Keine Glitches
- Keine doppelten Elemente sichtbar
## Debugging:
Falls Probleme auftreten:
- Browser DevTools: Prüfe, welche Elemente .expanded-menu-group oder .collapsed-menu-group haben
- Prüfe data-sidebar-expanded Attribut im HTML
- Prüfe z-index des Dropdowns (z-50)
- Prüfe, ob dropdown-right funktioniert
```
### Acceptance Criteria:
- ✅ menu_group und menu_subitem implementiert
- ✅ Details funktioniert (expanded)
- ✅ Dropdown funktioniert (collapsed)
- ✅ Flyout erscheint rechts
- ✅ Nur EIN Hover-Effekt
- ✅ Keine visuellen Glitches
---
## Task 9: Sidebar Footer mit Flexbox
**Agent:** Auto-Mode
**Geschätzte Dauer:** 25 Minuten
### Prompt:
```
Implementiere den Sidebar-Footer mit korrekter Positionierung am unteren Ende.
## Flexbox-Struktur:
Die Sidebar muss als Flexbox-Container funktionieren:
1. `.sidebar`: `flex flex-col` (bereits vorhanden)
2. Navigation: `flex-1` (nimmt verfügbaren Platz)
3. Footer: `mt-auto` (wird nach unten geschoben)
## Footer-Komponenten:
1. **Language Selector:**
- Nur im expanded state sichtbar (`.expanded-only`)
- DaisyUI select
- Form mit POST zu `/locale`
2. **Theme Toggle:**
- IMMER sichtbar
- Horizontal: Sun Icon + Toggle + Moon Icon
- DaisyUI toggle + theme-controller
3. **User Menu:**
- DaisyUI dropdown
- `dropdown-top dropdown-end` (öffnet nach oben)
- Avatar: Erste Buchstabe, zentriert, rund
- Email nur expanded
- Dropdown: Profile + Logout
## Implementierung:
```elixir
defp sidebar_footer(assigns) do
~H"""
<div class="mt-auto p-4 border-t border-base-300 space-y-4">
<!-- Language Selector (nur expanded) -->
<form method="post" action={~p"/locale"} class="expanded-only">
<input type="hidden" name="_csrf_token" value={get_csrf_token()} />
<select
name="locale"
onchange="this.form.submit()"
class="select select-sm w-full"
aria-label={gettext("Select language")}
>
<option value="de" selected={Gettext.get_locale() == "de"}>Deutsch</option>
<option value="en" selected={Gettext.get_locale() == "en"}>English</option>
</select>
</form>
<!-- Theme Toggle (immer sichtbar) -->
<.theme_toggle />
<!-- User Menu -->
<.user_menu current_user={@current_user} />
</div>
"""
end
defp theme_toggle(assigns) do
~H"""
<label class="flex items-center gap-2 cursor-pointer justify-center">
<.icon name="hero-sun" class="size-5" aria-hidden="true" />
<input
type="checkbox"
value="dark"
class="toggle toggle-sm theme-controller"
aria-label={gettext("Toggle dark mode")}
/>
<.icon name="hero-moon" class="size-5" aria-hidden="true" />
</label>
"""
end
defp user_menu(assigns) do
~H"""
<div class="dropdown dropdown-top dropdown-end w-full">
<button
type="button"
tabindex="0"
class="btn btn-ghost w-full flex items-center gap-3"
aria-label={gettext("User menu")}
aria-haspopup="menu"
>
<!-- Avatar: Zentriert, erste Buchstabe -->
<div class="avatar placeholder">
<div class="w-8 h-8 rounded-full bg-neutral text-neutral-content flex items-center justify-center">
<span class="text-sm font-medium">
{String.first(String.to_string(@current_user.email)) |> String.upcase()}
</span>
</div>
</div>
<span class="menu-label truncate flex-1 text-left">{String.to_string(@current_user.email)}</span>
</button>
<ul
tabindex="0"
class="dropdown-content menu bg-base-100 rounded-box shadow-lg z-50 w-52 p-2"
role="menu"
>
<li role="none">
<.link navigate={~p"/users/#{@current_user.id}"} role="menuitem">
{gettext("Profile")}
</.link>
</li>
<li role="none">
<.link href={~p"/sign-out"} role="menuitem">
{gettext("Logout")}
</.link>
</li>
</ul>
</div>
"""
end
```
Ersetze in `sidebar_content` den Footer-Placeholder:
```heex
<!-- Footer -->
<%= if @current_user do %>
<.sidebar_footer current_user={@current_user} />
<% end %>
```
Prüfe, dass `.sidebar_menu` `flex-1` hat:
```heex
<ul class="menu flex-1 w-full p-2" role="menubar">
```
## Testen:
1. **Positionierung:**
- Footer klebt am unteren Ende
- Auch bei wenigen Menu-Items
- Navigation wächst/schrumpft dynamisch
2. **Language Selector:**
- Erscheint nur expanded
- Verschwindet collapsed
- Form funktioniert (Sprache wechselt)
3. **Theme Toggle:**
- Immer sichtbar
- Icons horizontal ausgerichtet
- Toggle funktioniert
4. **User Menu:**
- Avatar zentriert (auch collapsed)
- Erste Buchstabe korrekt
- Email verschwindet collapsed
- Dropdown öffnet nach oben
- Profile + Logout Links funktionieren
5. **Collapsed State:**
- Footer bleibt am unteren Ende
- Avatar zentriert
- Theme Toggle funktioniert
```
### Acceptance Criteria:
- ✅ Footer am unteren Ende (Flexbox)
- ✅ Language Selector nur expanded
- ✅ Theme Toggle immer sichtbar
- ✅ User Menu mit Avatar
- ✅ Avatar zentriert in beiden States
- ✅ Dropdown öffnet nach oben
- ✅ Alle Links funktionieren
---
## Task 10: JavaScript State Management
**Agent:** Auto-Mode
**Geschätzte Dauer:** 15 Minuten
### Prompt:
```
Implementiere den vereinfachten SidebarState Hook in `assets/js/app.js`.
## Ziel:
Einfaches, robustes State-Management OHNE komplexe Event-Listener oder DOM-Manipulation.
## Anforderungen:
1. State in localStorage speichern
2. State via data-attribute auf HTML-Element setzen
3. Global `toggleSidebar()` Funktion bereitstellen
4. aria-expanded auf Toggle-Button aktualisieren
5. KEINE Checkbox-Manipulation
6. KEINE Tab-Index-Verwaltung
7. KEINE komplexen Event-Listener
## Implementierung:
Füge in `assets/js/app.js` zu den Hooks hinzu:
```javascript
Hooks.SidebarState = {
mounted() {
// Restore state from localStorage
const expanded = localStorage.getItem('sidebar-expanded') !== 'false';
this.setSidebarState(expanded);
// Expose toggle function globally
window.toggleSidebar = () => {
const current = this.el.dataset.sidebarExpanded === 'true';
this.setSidebarState(!current);
};
},
setSidebarState(expanded) {
// Update data-attribute (CSS reacts to this)
this.el.dataset.sidebarExpanded = expanded;
// Persist to localStorage
localStorage.setItem('sidebar-expanded', expanded);
// Update ARIA for accessibility
const toggleBtn = document.getElementById('sidebar-toggle');
if (toggleBtn) {
toggleBtn.setAttribute('aria-expanded', expanded);
}
},
destroyed() {
// Cleanup
delete window.toggleSidebar;
}
};
```
Stelle sicher, dass der Hook registriert ist:
```javascript
let liveSocket = new LiveSocket("/live", Socket, {
// ... existing config ...
hooks: Hooks
})
```
## Testen:
1. **Initial State:**
- Sidebar ist expanded (default)
- localStorage wird gesetzt
2. **Toggle:**
- Klick auf Toggle-Button
- Sidebar animiert zu collapsed
- localStorage wird aktualisiert
- Icon wechselt (Chevron-left ↔ Chevron-right)
3. **Persistence:**
- Toggle zu collapsed
- Seite neu laden
- Sidebar bleibt collapsed
4. **Browser Console:**
- Keine Fehler
- `window.toggleSidebar()` ist aufrufbar
- `localStorage.getItem('sidebar-expanded')` zeigt korrekten Wert
5. **ARIA:**
- Toggle-Button hat `aria-expanded="true"` oder `"false"`
- Wert ändert sich beim Toggle
## Debugging:
Falls Probleme:
```javascript
// In Browser Console:
console.log(document.documentElement.dataset.sidebarExpanded);
console.log(localStorage.getItem('sidebar-expanded'));
window.toggleSidebar(); // Manuell testen
```
```
### Acceptance Criteria:
- ✅ Hook implementiert
- ✅ State wird gespeichert (localStorage)
- ✅ Toggle funktioniert
- ✅ CSS reagiert auf data-attribute
- ✅ ARIA korrekt gesetzt
- ✅ Keine Console-Fehler
- ✅ Persistence funktioniert
---
## Task 11: Mobile Header Integration
**Agent:** Auto-Mode
**Geschätzte Dauer:** 10 Minuten
### Prompt:
```
Prüfe und optimiere die Mobile-Header-Integration.
## Anforderungen:
1. Mobile Header ist bereits in `layouts.ex` implementiert
2. Hamburger-Menü öffnet Sidebar
3. Header nur auf Mobile sichtbar (`lg:hidden`)
4. Overlay schließt Sidebar
## Zu prüfen:
Stelle sicher, dass in `layouts.ex` folgendes korrekt ist:
```heex
<header class="lg:hidden sticky top-0 z-10 navbar bg-base-100 shadow-sm">
<label for="mobile-drawer" class="btn btn-square btn-ghost" aria-label={gettext("Open navigation menu")}>
<.icon name="hero-bars-3" class="size-6" aria-hidden="true" />
</label>
<span class="font-bold">{@club_name}</span>
</header>
```
Und im drawer-side:
```heex
<label for="mobile-drawer" aria-label="close sidebar" class="drawer-overlay lg:hidden"></label>
```
## Testen:
1. **Mobile View (< 1024px):**
- Header erscheint am oberen Rand
- Hamburger-Icon sichtbar
- Club-Name neben Hamburger
2. **Hamburger-Click:**
- Sidebar schiebt von links rein
- Overlay verdunkelt Content
- Sidebar ist vollständig sichtbar
3. **Overlay-Click:**
- Sidebar schließt sich
- Overlay verschwindet
- Content wieder normal
4. **Desktop View (≥ 1024px):**
- Header verschwindet
- Sidebar ist persistent sichtbar
- Kein Hamburger-Icon
- Toggle-Button erscheint in Sidebar
5. **Responsive Breakpoint:**
- Smooth Transition bei Resize
- Keine Layout-Breaks
- Sidebar-State bleibt erhalten
## Optional: CSS-Verbesserungen
Falls Mobile-Sidebar zu schmal ist, füge in `app.css` hinzu:
```css
/* Mobile Drawer Width */
@media (max-width: 1023px) {
.drawer-side .sidebar {
width: 16rem; /* w-64 auch auf Mobile */
}
}
```
```
### Acceptance Criteria:
- ✅ Mobile Header funktioniert
- ✅ Hamburger öffnet Sidebar
- ✅ Overlay schließt Sidebar
- ✅ Header nur auf Mobile
- ✅ Responsive funktioniert
- ✅ Keine Layout-Breaks
---
## Task 12: Tests & Accessibility
**Agent:** Sonnet 4.5
**Geschätzte Dauer:** 30 Minuten
### Prompt:
```
Schreibe und aktualisiere Tests für die neue Sidebar-Implementierung in `test/mv_web/components/layouts/sidebar_test.exs`.
## Test-Kategorien:
### 1. Component Tests
```elixir
describe "sidebar_header/1" do
test "renders logo with correct size" do
# Logo ist size-8 (32px)
# Logo ist immer sichtbar
end
test "renders club name" do
# Club name ist vorhanden
end
test "renders toggle button for desktop" do
# Toggle button hat beide Icons
# Toggle button ist nicht mobile
end
end
describe "menu_item/1" do
test "renders icon and label" do
# Icon ist sichtbar
# Label ist vorhanden
end
test "has tooltip with correct text" do
# data-tip attribute gesetzt
end
test "has correct link" do
# navigate attribute korrekt
end
end
describe "menu_group/1" do
test "renders expanded menu group" do
# details/summary vorhanden
end
test "renders collapsed menu group" do
# dropdown vorhanden
end
test "renders submenu items" do
# Inner_block items gerendert
end
end
describe "sidebar_footer/1" do
test "renders at bottom of sidebar" do
# mt-auto vorhanden
end
test "renders theme toggle" do
# Toggle ist immer sichtbar
end
test "renders language selector in expanded only" do
# expanded-only Klasse
end
test "renders user menu with avatar" do
# Avatar vorhanden
# Erste Buchstabe korrekt
end
end
```
### 2. Integration Tests
```elixir
describe "sidebar state management" do
test "sidebar starts expanded by default" do
# data-sidebar-expanded="true"
end
test "toggle button changes state" do
# Simulate toggle
# data-sidebar-expanded ändert sich
end
test "no duplicate IDs in layout" do
# field-visibility-dropdown nur einmal
# Keine duplicate IDs
end
end
describe "mobile drawer" do
test "mobile header renders on small screens" do
# lg:hidden Klasse
end
test "drawer toggle opens sidebar" do
# drawer-toggle funktioniert
end
end
```
### 3. Accessibility Tests
```elixir
describe "accessibility" do
test "sidebar has aria-label" do
# aria-label="Main navigation"
end
test "toggle button has aria-label and aria-expanded" do
# aria-label vorhanden
# aria-expanded="true" or "false"
end
test "menu items have role attributes" do
# role="menubar", "menuitem", "menu"
end
test "icons have aria-hidden" do
# Dekorative Icons: aria-hidden="true"
end
test "user menu has aria-haspopup" do
# aria-haspopup="menu"
end
end
```
### 4. Regression Tests
```elixir
describe "regression tests" do
test "no duplicate profile links" do
# Nur ein Profile-Link im DOM
end
test "nested menu has only one hover effect" do
# Nicht doppelter Hover
end
test "tooltips only visible when collapsed" do
# CSS opacity rules
end
end
```
## Testen:
Führe alle Tests aus:
```bash
mix test test/mv_web/components/layouts/sidebar_test.exs
```
Prüfe:
- Alle Tests bestehen
- Keine Warnungen
- Test-Coverage akzeptabel
Falls Tests fehlschlagen:
1. Analysiere Fehler
2. Fixe Code oder Test
3. Wiederhole bis alle grün sind
## Accessibility Tools:
Optional: Nutze Browser DevTools
- Lighthouse Accessibility Audit
- Axe DevTools Extension
- WAVE Extension
```
### Acceptance Criteria:
- ✅ Component Tests geschrieben
- ✅ Integration Tests geschrieben
- ✅ Accessibility Tests geschrieben
- ✅ Regression Tests geschrieben
- ✅ Alle Tests bestehen
- ✅ Keine Test-Warnungen
---
## Task 13: Visual Testing & Bug Fixes
**Agent:** Manuell (+ Sonnet 4.5 für Fixes)
**Geschätzte Dauer:** 30 Minuten
### Checklist für manuelle Tests:
```
## Desktop - Expanded State
- [ ] Logo ist 32px groß (size-8)
- [ ] Logo ist zentriert vertikal
- [ ] Club-Name ist sichtbar neben Logo
- [ ] Toggle-Button ist sichtbar
- [ ] Toggle-Button zeigt Chevron-Left Icon
- [ ] Alle Menü-Items zeigen Icon + Text
- [ ] Beiträge-Menü ist als Details/Summary dargestellt
- [ ] Footer ist am unteren Ende
- [ ] Language-Selector ist sichtbar
- [ ] Theme-Toggle ist horizontal (Sun + Toggle + Moon)
- [ ] User-Avatar ist rund und zentriert
- [ ] Email ist neben Avatar sichtbar
## Desktop - Collapsed State
- [ ] Logo ist 32px groß (gleich wie expanded!)
- [ ] Logo ist zentriert vertikal UND horizontal
- [ ] Club-Name ist NICHT sichtbar
- [ ] Toggle-Button ist sichtbar
- [ ] Toggle-Button zeigt Chevron-Right Icon
- [ ] Alle Menü-Items zeigen nur Icons (zentriert)
- [ ] Tooltips erscheinen bei Hover über Icons
- [ ] Tooltips zeigen korrekten Text
- [ ] Beiträge-Menü ist als Dropdown dargestellt
- [ ] Dropdown öffnet RECHTS vom Icon
- [ ] Dropdown enthält "Beiträge" als Titel
- [ ] Footer ist am unteren Ende
- [ ] Language-Selector ist NICHT sichtbar
- [ ] Theme-Toggle ist sichtbar und funktioniert
- [ ] User-Avatar ist zentriert (horizontal)
- [ ] Email ist NICHT sichtbar
- [ ] Sidebar-Breite ist 4rem (w-16)
## Toggle-Funktion
- [ ] Click auf Toggle-Button wechselt State
- [ ] Transition ist smooth (300ms)
- [ ] Keine Glitches während Transition
- [ ] Icon wechselt smooth
- [ ] State wird in localStorage gespeichert
- [ ] Nach Reload bleibt State erhalten
## Mobile (< 1024px)
- [ ] Header mit Hamburger-Icon erscheint oben
- [ ] Club-Name neben Hamburger
- [ ] Desktop Toggle-Button ist NICHT sichtbar
- [ ] Click auf Hamburger öffnet Drawer
- [ ] Sidebar schiebt von links rein
- [ ] Overlay verdunkelt Hintergrund
- [ ] Click auf Overlay schließt Drawer
- [ ] Sidebar-Content ist vollständig sichtbar
- [ ] Alle Menü-Items funktionieren
- [ ] Footer ist am unteren Ende
## Hover-Effekte
- [ ] Menü-Items: Einheitlicher Hover (hellerer Hintergrund)
- [ ] Beiträge-Menü (expanded): Nur EIN Hover auf Summary
- [ ] Beiträge-Menü (collapsed): Nur EIN Hover auf Button
- [ ] Toggle-Button: Hover-Effekt vorhanden
- [ ] User-Menu: Hover-Effekt vorhanden
- [ ] Keine doppelten oder konflikthaften Hovers
## Nested Menu "Beiträge"
- [ ] Expanded: Details/Summary funktioniert
- [ ] Expanded: Click öffnet/schließt Submenu
- [ ] Expanded: Submenu-Items eingerückt (ml-4)
- [ ] Collapsed: Dropdown-Button zentriert
- [ ] Collapsed: Tooltip zeigt "Beiträge"
- [ ] Collapsed: Click öffnet Dropdown RECHTS
- [ ] Collapsed: Dropdown hat "Beiträge" als Titel
- [ ] Collapsed: Submenu-Items klickbar
- [ ] Collapsed: Dropdown schließt nach Click außerhalb
## Footer-Positionierung
- [ ] Footer klebt am unteren Ende (auch bei wenigen Items)
- [ ] Navigation (flex-1) wächst/schrumpft korrekt
- [ ] Kein Leerraum zwischen Navigation und Footer
- [ ] Footer bleibt am unteren Ende beim Scroll
## Transitions & Animations
- [ ] Width-Transition: smooth, 300ms
- [ ] Label Fade-out: smooth, 200ms
- [ ] Icon-Swap: instant (kein Flicker)
- [ ] Keine Layout-Shifts
- [ ] Keine Horizontal-Scrollbar während Transition
## Browser-Kompatibilität
- [ ] Chrome/Edge: Alles funktioniert
- [ ] Firefox: Alles funktioniert
- [ ] Safari: Alles funktioniert (falls verfügbar)
## Accessibility (Screen Reader)
- [ ] Toggle-Button: aria-expanded wird angesagt
- [ ] Menü-Items: Links werden korrekt angesagt
- [ ] User-Menu: Dropdown wird als Menü erkannt
- [ ] Icons: Nicht vom Screen Reader angesagt (aria-hidden)
## Performance
- [ ] Keine Console-Errors
- [ ] Keine Console-Warnings
- [ ] localStorage funktioniert
- [ ] Keine Memory-Leaks (DevTools Memory Profiler)
```
### Bug-Fixing Prompt (falls Bugs gefunden):
```
Ich habe folgende Bugs bei der visuellen Prüfung der Sidebar gefunden:
[LISTE DER BUGS HIER EINFÜGEN]
Bitte analysiere und fixe diese Bugs:
1. Identifiziere die Ursache für jeden Bug
2. Erstelle Fixes in den betroffenen Dateien
3. Teste, dass der Fix funktioniert
4. Stelle sicher, dass keine Regression entsteht
Betroffene Dateien könnten sein:
- lib/mv_web/components/layouts.ex
- lib/mv_web/components/layouts/sidebar.ex
- assets/css/app.css
- assets/js/app.js
Prüfe besonders:
- CSS-Selektoren ([data-sidebar-expanded])
- Flexbox-Layout (flex-col, flex-1, mt-auto)
- Tailwind-Klassen (lg:hidden, lg:flex, etc.)
- DaisyUI-Komponenten (dropdown, menu, tooltip)
```
### Acceptance Criteria:
- ✅ Alle Checkboxen abgehakt
- ✅ Keine visuellen Bugs
- ✅ Alle Anforderungen erfüllt
- ✅ Performance akzeptabel
- ✅ Accessibility getestet
---
## Finale Dokumentation
Nach erfolgreicher Umsetzung aller Tasks:
### Prompt für Dokumentations-Update:
```
Aktualisiere die Projekt-Dokumentation basierend auf der neuen Sidebar-Implementierung:
1. Update `CODE_GUIDELINES.md`:
- Sektion "Frontend: Tailwind CSS & DaisyUI"
- Beschreibe das Sidebar Pattern
- Dokumentiere State-Management Ansatz
2. Erstelle `docs/sidebar-architecture.md`:
- Übersicht über Komponenten
- State-Management Erklärung
- CSS-Strategie
- Troubleshooting Guide
3. Update `docs/feature-roadmap.md`:
- Markiere Sidebar als abgeschlossen
- Dokumentiere Learnings
4. Erstelle `docs/sidebar-maintenance.md`:
- Wie man neue Menü-Items hinzufügt
- Wie man Nested Menus hinzufügt
- Wie man Styling anpasst
- Häufige Probleme und Lösungen
```
---
## Zusammenfassung
**Gesamtdauer:** ~4-5 Stunden (mit Testing & Bug Fixing)
**Kritische Tasks (Sonnet 4.5):**
- Task 1: Analyse
- Task 2: Recherche
- Task 3: Design
- Task 8: Nested Menu
- Task 12: Testing
**Auto-Mode Tasks:**
- Task 4-7: Foundation & Basic Components
- Task 9-11: Footer & Integration
**Manuell:**
- Task 13: Visual QA
**Erfolgskriterien:**
- ✅ Alle 10 ursprünglichen Anforderungen erfüllt
- ✅ Keine Custom CSS Variants
- ✅ 100% DaisyUI Standard
- ✅ Keine duplicate IDs
- ✅ Accessibility WCAG 2.1 AA
- ✅ Alle Tests grün
- ✅ Performance gut
- ✅ Wartbar und dokumentiert
---
**Version:** 1.0
**Letzte Aktualisierung:** 2025-12-16