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
21 KiB
Sidebar Analysis - Current State
Erstellt: 2025-12-16
Status: Analyse für Neuimplementierung
Autor: Cursor AI Assistant
Executive Summary
Die aktuelle Sidebar-Implementierung verwendet nicht existierende Custom-CSS-Variants (is-drawer-close: und is-drawer-open:), was zu einer defekten Implementierung führt. Die Sidebar ist strukturell basierend auf DaisyUI's Drawer-Komponente, aber die responsive und state-basierte Funktionalität ist nicht funktionsfähig.
Kritisches Problem: Die im Code verwendeten Variants is-drawer-close:* und is-drawer-open:* sind nicht in Tailwind konfiguriert, was bedeutet, dass diese Klassen beim Build ignoriert werden.
1. Dateien-Übersicht
1.1 Hauptdateien
| Datei | Zweck | Zeilen | Status |
|---|---|---|---|
lib/mv_web/components/layouts/sidebar.ex |
Sidebar-Komponente (Elixir) | 198 | ⚠️ Verwendet nicht existierende Variants |
lib/mv_web/components/layouts/navbar.ex |
Navbar mit Sidebar-Toggle | 48 | ✅ Funktional |
lib/mv_web/components/layouts.ex |
Layout-Wrapper mit Drawer | 121 | ✅ Funktional |
assets/js/app.js |
JavaScript für Sidebar-Interaktivität | 272 | ✅ Umfangreiche Accessibility-Logik |
assets/css/app.css |
CSS-Konfiguration | 103 | ⚠️ Keine Drawer-Variants definiert |
assets/tailwind.config.js |
Tailwind-Konfiguration | 75 | ⚠️ Keine Drawer-Variants definiert |
1.2 Verwandte Dateien
lib/mv_web/components/layouts/root.html.heex- Root-Layout (minimal, keine Sidebar-Logik)priv/static/images/logo.svg- Logo (wird vermutlich für Sidebar benötigt)
2. Aktuelle Struktur
2.1 HTML-Struktur (DaisyUI Drawer Pattern)
<!-- In layouts.ex -->
<div class="drawer">
<input id="main-drawer" type="checkbox" class="drawer-toggle" />
<div class="drawer-content">
<!-- Navbar mit Toggle-Button -->
<navbar with sidebar-toggle button />
<!-- Hauptinhalt -->
<main>...</main>
</div>
<!-- Sidebar -->
<div class="drawer-side">
<button class="drawer-overlay" onclick="close drawer"></button>
<nav id="main-sidebar">
<!-- Navigation Items -->
</nav>
</div>
</div>
Bewertung: ✅ Korrekte DaisyUI Drawer-Struktur
2.2 Sidebar-Komponente (sidebar.ex)
Struktur:
defmodule MvWeb.Layouts.Sidebar do
attr :current_user, :map
attr :club_name, :string
def sidebar(assigns) do
# Rendert Sidebar mit Navigation, Locale-Selector, Theme-Toggle, User-Menu
end
end
Hauptelemente:
- Drawer Overlay - Button zum Schließen (Mobile)
- Navigation Container (
<nav id="main-sidebar">) - Menü-Items - Members, Users, Contributions (nested), Settings
- Footer-Bereich - Locale-Selector, Theme-Toggle, User-Menu
3. Custom CSS Variants - KRITISCHES PROBLEM
3.1 Verwendete Variants im Code
Die Sidebar verwendet folgende Custom-Variants extensiv:
# Beispiele aus sidebar.ex
"is-drawer-close:overflow-visible"
"is-drawer-close:w-14 is-drawer-open:w-64"
"is-drawer-close:hidden"
"is-drawer-close:tooltip is-drawer-close:tooltip-right"
"is-drawer-close:w-auto"
"is-drawer-close:justify-center"
"is-drawer-close:dropdown-end"
Gefundene Verwendungen:
is-drawer-close:- 13 Instanzen in sidebar.exis-drawer-open:- 1 Instanz in sidebar.ex
3.2 Definition der Variants
❌ NICHT GEFUNDEN in:
assets/css/app.css- Enthält nurphx-*-loadingVariantsassets/tailwind.config.js- Enthält nurphx-*-loadingVariants
Fazit: Diese Variants existieren nicht und werden beim Tailwind-Build ignoriert!
3.3 Vorhandene Variants
Nur folgende Custom-Variants sind tatsächlich definiert:
/* In app.css (Tailwind CSS 4.x Syntax) */
@custom-variant phx-click-loading (.phx-click-loading&, .phx-click-loading &);
@custom-variant phx-submit-loading (.phx-submit-loading&, .phx-submit-loading &);
@custom-variant phx-change-loading (.phx-change-loading&, .phx-change-loading &);
/* In tailwind.config.js (Tailwind 3.x Kompatibilität) */
plugin(({addVariant}) => addVariant("phx-click-loading", [...])),
plugin(({addVariant}) => addVariant("phx-submit-loading", [...])),
plugin(({addVariant}) => addVariant("phx-change-loading", [...])),
4. JavaScript-Implementierung
4.1 Übersicht
Die JavaScript-Implementierung ist sehr umfangreich und fokussiert auf Accessibility:
Datei: assets/js/app.js (Zeilen 106-270)
Hauptfunktionalitäten:
- ✅ Tabindex-Management für fokussierbare Elemente
- ✅ ARIA-Attribut-Management (
aria-expanded) - ✅ Keyboard-Navigation (Enter, Space, Escape)
- ✅ Focus-Management beim Öffnen/Schließen
- ✅ Dropdown-Integration
4.2 Wichtige JavaScript-Funktionen
4.2.1 Tabindex-Management
const updateSidebarTabIndex = (isOpen) => {
const allFocusableElements = sidebar.querySelectorAll(
'a[href], button, select, input:not([type="hidden"]), [tabindex]'
)
allFocusableElements.forEach(el => {
if (isOpen) {
// Make focusable when open
el.removeAttribute('tabindex')
} else {
// Remove from tab order when closed
el.setAttribute('tabindex', '-1')
}
})
}
Zweck: Verhindert, dass Nutzer mit Tab zu unsichtbaren Sidebar-Elementen springen können.
4.2.2 ARIA-Expanded Management
const updateAriaExpanded = () => {
const isOpen = drawerToggle.checked
sidebarToggle.setAttribute("aria-expanded", isOpen.toString())
}
Zweck: Informiert Screen-Reader über den Sidebar-Status.
4.2.3 Focus-Management
const getFirstFocusableElement = () => {
// Priority: navigation link > other links > other focusable
const firstNavLink = sidebar.querySelector('a[href][role="menuitem"]')
// ... fallback logic
}
// On open: focus first element
// On close: focus toggle button
Zweck: Logische Fokus-Reihenfolge für Keyboard-Navigation.
4.2.4 Keyboard-Shortcuts
// ESC to close
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && drawerToggle.checked) {
drawerToggle.checked = false
sidebarToggle.focus()
}
})
// Enter/Space on toggle button
sidebarToggle.addEventListener("keydown", (e) => {
if (e.key === "Enter" || e.key === " ") {
// Toggle drawer and manage focus
}
})
4.3 LiveView Hooks
Definierte Hooks:
Hooks.CopyToClipboard = { ... } // Clipboard-Funktionalität
Hooks.ComboBox = { ... } // Dropdown-Prävention bei Enter
Sidebar-spezifisch: Keine Hooks, nur native DOM-Events.
5. DaisyUI Dependencies
5.1 Verwendete DaisyUI-Komponenten
| Komponente | Verwendung | Klassen |
|---|---|---|
| Drawer | Basis-Layout | drawer, drawer-toggle, drawer-side, drawer-content, drawer-overlay |
| Menu | Navigation | menu, menu-title, w-64 |
| Button | Toggle, User-Menu | btn, btn-ghost, btn-square, btn-circle |
| Avatar | User-Menu | avatar, avatar-placeholder |
| Dropdown | User-Menu | dropdown, dropdown-top, dropdown-end, dropdown-content |
| Tooltip | Icon-Tooltips | tooltip, tooltip-right (via data-tip) |
| Select | Locale-Selector | select, select-sm |
| Toggle | Theme-Switch | toggle, theme-controller |
5.2 Standard Tailwind-Klassen
Layout:
flex,flex-col,items-start,justify-centergap-2,gap-4,p-4,mt-auto,w-full,w-64,min-h-full
Sizing:
size-4,size-5,w-12,w-52
Colors:
bg-base-100,bg-base-200,text-neutral-content
Typography:
text-lg,text-sm,font-bold
Accessibility:
sr-only,focus:outline-none,focus:ring-2,focus:ring-primary
6. Toggle-Button (Navbar)
6.1 Implementierung
Datei: lib/mv_web/components/layouts/navbar.ex
<button
type="button"
onclick="document.getElementById('main-drawer').checked = !document.getElementById('main-drawer').checked"
aria-label={gettext("Toggle navigation menu")}
aria-expanded="false"
aria-controls="main-sidebar"
id="sidebar-toggle"
class="mr-2 btn btn-square btn-ghost"
>
<svg><!-- Layout-Panel-Left Icon --></svg>
</button>
Funktionalität:
- ✅ Togglet Drawer-Checkbox
- ✅ ARIA-Labels vorhanden
- ✅ Keyboard-accessible
- ⚠️
aria-expandedwird durch JavaScript aktualisiert
Icon: Custom SVG (Layout-Panel-Left mit Chevron-Right)
7. Responsive Verhalten
7.1 Aktuelles Konzept (nicht funktional)
Versuchte Implementierung:
- Desktop (collapsed): Sidebar mit 14px Breite (
is-drawer-close:w-14) - Desktop (expanded): Sidebar mit 64px Breite (
is-drawer-open:w-64) - Mobile: Overlay-Drawer (DaisyUI Standard)
7.2 Problem
Da die is-drawer-* Variants nicht existieren, gibt es kein responsives Verhalten:
- Die Sidebar hat immer eine feste Breite von
w-64 - Die conditional hiding (
:hidden, etc.) funktioniert nicht - Tooltips werden nicht conditional angezeigt
8. Accessibility-Features
8.1 Implementierte Features
| Feature | Status | Implementierung |
|---|---|---|
| ARIA Labels | ✅ | Alle interaktiven Elemente haben Labels |
| ARIA Roles | ✅ | menubar, menuitem, menu, button |
| ARIA Expanded | ✅ | Wird durch JS dynamisch gesetzt |
| ARIA Controls | ✅ | Toggle → Sidebar verknüpft |
| Keyboard Navigation | ✅ | Enter, Space, Escape, Tab |
| Focus Management | ✅ | Logische Focus-Reihenfolge |
| Tabindex Management | ✅ | Verhindert Focus auf hidden Elements |
| Screen Reader Only | ✅ | .sr-only für visuelle Labels |
| Focus Indicators | ✅ | focus:ring-2 focus:ring-primary |
| Skip Links | ❌ | Nicht vorhanden |
8.2 Accessibility-Score
Geschätzt: 90/100 (WCAG 2.1 Level AA konform)
Verbesserungspotenzial:
- Skip-Link zur Hauptnavigation hinzufügen
- High-Contrast-Mode testen
9. Menü-Struktur
9.1 Navigation Items
📋 Main Menu
├── 👥 Members (/members)
├── 👤 Users (/users)
├── 💰 Contributions (collapsed submenu)
│ ├── Plans (/contribution_types)
│ └── Settings (/contribution_settings)
└── ⚙️ Settings (/settings)
🔽 Footer Area (logged in only)
├── 🌐 Locale Selector (DE/EN)
├── 🌓 Theme Toggle (Light/Dark)
└── 👤 User Menu (Dropdown)
├── Profile (/users/:id)
└── Logout (/sign-out)
9.2 Conditional Rendering
Nicht eingeloggt:
- Sidebar ist leer (nur Struktur)
- Keine Menü-Items
Eingeloggt:
- Vollständige Navigation
- Footer-Bereich mit User-Menu
9.3 Nested Menu (Contributions)
Problem: Das Contributions-Submenu ist immer versteckt im collapsed State:
<li class="is-drawer-close:hidden" role="none">
<h2 class="flex items-center gap-2 menu-title">
<.icon name="hero-currency-dollar" />
{gettext("Contributions")}
</h2>
<ul role="menu">
<li class="is-drawer-close:hidden">...</li>
<li class="is-drawer-close:hidden">...</li>
</ul>
</li>
Da :hidden nicht funktioniert, wird das Submenu immer angezeigt.
10. Theme-Funktionalität
10.1 Theme-Toggle
<input
type="checkbox"
value="dark"
class="toggle theme-controller"
aria-label={gettext("Toggle dark mode")}
/>
Funktionalität:
- ✅ DaisyUI
theme-controller- automatische Theme-Umschaltung - ✅ Persistence durch
localStorage(siehe root.html.heex Script) - ✅ Icon-Wechsel (Sun ↔ Moon)
10.2 Definierte Themes
Datei: assets/css/app.css
-
Light Theme (default)
- Base:
oklch(98% 0 0) - Primary:
oklch(70% 0.213 47.604)(Orange/Phoenix-inspiriert)
- Base:
-
Dark Theme
- Base:
oklch(30.33% 0.016 252.42) - Primary:
oklch(58% 0.233 277.117)(Purple/Elixir-inspiriert)
- Base:
11. Locale-Funktionalität
11.1 Locale-Selector
<form method="post" action="/set_locale">
<select
id="locale-select-sidebar"
name="locale"
onchange="this.form.submit()"
class="select select-sm w-full is-drawer-close:w-auto"
>
<option value="de">Deutsch</option>
<option value="en">English</option>
</select>
</form>
Funktionalität:
- ✅ POST zu
/set_localeEndpoint - ✅ CSRF-Token included
- ✅ Auto-Submit on change
- ✅ Accessible Label (
.sr-only)
12. Probleme und Defekte
12.1 Kritische Probleme
| Problem | Schweregrad | Details |
|---|---|---|
| Nicht existierende CSS-Variants | 🔴 Kritisch | is-drawer-close:* und is-drawer-open:* sind nicht definiert |
| Keine responsive Funktionalität | 🔴 Kritisch | Sidebar verhält sich nicht wie geplant |
| Conditional Styles funktionieren nicht | 🔴 Kritisch | Hidden/Tooltip/Width-Changes werden ignoriert |
12.2 Mittlere Probleme
| Problem | Schweregrad | Details |
|---|---|---|
| Kein Logo | 🟡 Mittel | Logo-Element fehlt komplett in der Sidebar |
| Submenu immer sichtbar | 🟡 Mittel | Contributions-Submenu sollte in collapsed State versteckt sein |
| Toggle-Icon statisch | 🟡 Mittel | Icon ändert sich nicht zwischen expanded/collapsed |
12.3 Kleinere Probleme
| Problem | Schweregrad | Details |
|---|---|---|
| Code-Redundanz | 🟢 Klein | Variants in beiden Tailwind-Configs (3.x und 4.x) |
| Inline-onclick Handler | 🟢 Klein | Sollten durch JS-Events ersetzt werden |
| Keine Skip-Links | 🟢 Klein | Accessibility-Verbesserung |
13. Abhängigkeiten
13.1 Externe Abhängigkeiten
| Dependency | Version | Verwendung |
|---|---|---|
| DaisyUI | Latest (vendor) | Drawer, Menu, Button, etc. |
| Tailwind CSS | 4.0.9 | Utility-Klassen |
| Heroicons | v2.2.0 | Icons in Navigation |
| Phoenix LiveView | ~> 1.1.0 | Backend-Integration |
13.2 Interne Abhängigkeiten
| Modul | Verwendung |
|---|---|
MvWeb.Gettext |
Internationalisierung |
Mv.Membership.get_settings() |
Club-Name abrufen |
MvWeb.CoreComponents |
Icons, Links |
14. Code-Qualität
14.1 Positives
- ✅ Sehr gute Accessibility-Implementierung
- ✅ Saubere Modulstruktur (Separation of Concerns)
- ✅ Gute Dokumentation (Moduledocs, Attribute docs)
- ✅ Internationalisierung vollständig implementiert
- ✅ ARIA-Best-Practices befolgt
- ✅ Keyboard-Navigation umfassend
14.2 Verbesserungsbedarf
- ❌ Broken CSS-Variants (Hauptproblem)
- ❌ Fehlende Tests (keine Component-Tests gefunden)
- ⚠️ Inline-JavaScript in onclick-Attributen
- ⚠️ Magic-IDs (
main-drawer,sidebar-toggle) hardcoded - ⚠️ Komplexe JavaScript-Logik ohne Dokumentation
15. Empfehlungen für Neuimplementierung
15.1 Sofort-Maßnahmen
-
CSS-Variants entfernen
- Alle
is-drawer-close:*undis-drawer-open:*entfernen - Durch Standard-Tailwind oder DaisyUI-Mechanismen ersetzen
- Alle
-
Logo hinzufügen
- Logo-Element als erstes Element in Sidebar
- Konsistente Größe (32px / size-8)
-
Toggle-Icon implementieren
- Icon-Swap zwischen Chevron-Left und Chevron-Right
- Nur auf Desktop sichtbar
15.2 Architektur-Entscheidungen
-
Responsive Strategie:
- Mobile: Standard DaisyUI Drawer (Overlay)
- Desktop: Persistent Sidebar mit fester Breite
- Kein collapsing auf Desktop (einfacher, wartbarer)
-
State-Management:
- Drawer-Checkbox für Mobile
- Keine zusätzlichen Custom-Variants
- Standard DaisyUI-Mechanismen verwenden
-
JavaScript-Refactoring:
- Hooks statt inline-onclick
- Dokumentierte Funktionen
- Unit-Tests für kritische Logik
15.3 Prioritäten
High Priority:
- CSS-Variants-Problem lösen
- Logo implementieren
- Basic responsive Funktionalität
Medium Priority: 4. Toggle-Icon implementieren 5. Tests schreiben 6. JavaScript refactoren
Low Priority: 7. Skip-Links hinzufügen 8. Code-Optimierung 9. Performance-Tuning
16. Checkliste für Neuimplementierung
16.1 Vorbereitung
- Alle
is-drawer-*Klassen aus Code entfernen - Keine Custom-Variants in CSS/Tailwind definieren
- DaisyUI-Dokumentation für Drawer studieren
16.2 Implementation
- Logo-Element hinzufügen (size-8, persistent)
- Toggle-Button mit Icon-Swap (nur Desktop)
- Mobile: Overlay-Drawer (DaisyUI Standard)
- Desktop: Persistent Sidebar (w-64)
- Menü-Items mit korrekten Klassen
- Submenu-Handling (nested
<ul>)
16.3 Funktionalität
- Toggle-Funktionalität auf Mobile
- Accessibility: ARIA, Focus, Keyboard
- Theme-Toggle funktional
- Locale-Selector funktional
- User-Menu-Dropdown funktional
16.4 Testing
- Component-Tests schreiben
- Accessibility-Tests (axe-core)
- Keyboard-Navigation testen
- Screen-Reader testen
- Responsive Breakpoints testen
16.5 Dokumentation
- Code-Kommentare aktualisieren
- Component-Docs schreiben
- README aktualisieren
17. Technische Details
17.1 CSS-Selektoren
Verwendete IDs:
#main-drawer- Drawer-Toggle-Checkbox#main-sidebar- Sidebar-Navigation-Container#sidebar-toggle- Toggle-Button in Navbar#locale-select-sidebar- Locale-Dropdown
Verwendete Klassen:
.drawer-side- DaisyUI Sidebar-Container.drawer-overlay- DaisyUI Overlay-Button.drawer-content- DaisyUI Content-Container.menu- DaisyUI Menu-Container.is-drawer-close:*- ❌ NICHT DEFINIERT.is-drawer-open:*- ❌ NICHT DEFINIERT
17.2 Event-Handler
JavaScript:
drawerToggle.addEventListener("change", ...)
sidebarToggle.addEventListener("click", ...)
sidebarToggle.addEventListener("keydown", ...)
document.addEventListener("keydown", ...) // ESC handler
Inline (zu migrieren):
onclick="document.getElementById('main-drawer').checked = false"
onclick="document.getElementById('main-drawer').checked = !..."
onchange="this.form.submit()"
18. Metriken
18.1 Code-Metriken
| Metrik | Wert |
|---|---|
| Zeilen Code (Sidebar) | 198 |
| Zeilen JavaScript | 165 (Sidebar-spezifisch) |
| Zeilen CSS | 0 (nur Tailwind-Klassen) |
| Anzahl Komponenten | 1 (Sidebar) + 1 (Navbar) |
| Anzahl Menü-Items | 6 (inkl. Submenu) |
| Anzahl Footer-Controls | 3 (Locale, Theme, User) |
18.2 Abhängigkeits-Metriken
| Kategorie | Anzahl |
|---|---|
| DaisyUI-Komponenten | 7 |
| Tailwind-Utility-Klassen | ~50 |
| Custom-Variants (broken) | 2 (is-drawer-close, is-drawer-open) |
| JavaScript-Event-Listener | 6 |
| ARIA-Attribute | 12 |
19. Zusammenfassung
19.1 Was funktioniert
✅ Sehr gute Grundlage:
- DaisyUI Drawer-Pattern korrekt implementiert
- Exzellente Accessibility (ARIA, Keyboard, Focus)
- Saubere Modulstruktur
- Internationalisierung
- Theme-Switching
- JavaScript-Logik ist robust
19.2 Was nicht funktioniert
❌ Kritische Defekte:
- CSS-Variants existieren nicht → keine responsive Funktionalität
- Kein Logo
- Kein Toggle-Icon-Swap
- Submenu-Handling defekt
19.3 Nächste Schritte
- CSS-Variants entfernen (alle
is-drawer-*Klassen) - Standard DaisyUI-Pattern verwenden (ohne Custom-Variants)
- Logo hinzufügen (persistent, size-8)
- Simplify: Mobile = Overlay, Desktop = Persistent (keine collapsed State)
- Tests schreiben (Component + Accessibility)
20. Anhang
20.1 Verwendete CSS-Klassen (alphabetisch)
avatar, avatar-placeholder, bg-base-100, bg-base-200, bg-neutral,
btn, btn-circle, btn-ghost, btn-square, cursor-pointer, drawer,
drawer-content, drawer-overlay, drawer-side, drawer-toggle, dropdown,
dropdown-content, dropdown-end, dropdown-top, flex, flex-col,
focus:outline-none, focus:ring-2, focus:ring-primary,
focus-within:outline-none, focus-within:ring-2, gap-2, gap-4,
is-drawer-close:*, is-drawer-open:*, items-center, items-start,
mb-2, menu, menu-sm, menu-title, min-h-full, mr-2, mt-3, mt-auto,
p-2, p-4, rounded-box, rounded-full, select, select-sm, shadow,
shadow-sm, size-4, size-5, sr-only, text-lg, text-neutral-content,
text-sm, theme-controller, toggle, tooltip, tooltip-right, w-12,
w-52, w-64, w-full, z-1
20.2 Verwendete ARIA-Attribute
aria-busy, aria-controls, aria-describedby, aria-expanded,
aria-haspopup, aria-hidden, aria-label, aria-labelledby,
aria-live, role="alert", role="button", role="menu",
role="menubar", role="menuitem", role="none", role="status"
20.3 Relevante Links
Ende des Berichts