From 16ca4efc0318c365d3aa86c8815ae7ba9c49d6bd Mon Sep 17 00:00:00 2001 From: Simon Date: Thu, 18 Dec 2025 16:33:44 +0100 Subject: [PATCH] feat: implement standard-compliant sidebar with comprehensive tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- assets/css/app.css | 134 ++ assets/js/app.js | 39 +- docs/daisyui-drawer-pattern.md | 532 ++++++ docs/sidebar-analysis-current-state.md | 746 ++++++++ docs/sidebar-requirements-v2.md | 1250 +++++++++++++ docs/umsetzung-sidebar.md | 1576 +++++++++++++++++ lib/mv_web/components/layouts.ex | 47 +- lib/mv_web/components/layouts/sidebar.ex | 454 +++-- priv/static/images/mila.svg | 5 + .../components/layouts/sidebar_test.exs | 850 +++++++++ 10 files changed, 5439 insertions(+), 194 deletions(-) create mode 100644 docs/daisyui-drawer-pattern.md create mode 100644 docs/sidebar-analysis-current-state.md create mode 100644 docs/sidebar-requirements-v2.md create mode 100644 docs/umsetzung-sidebar.md create mode 100644 priv/static/images/mila.svg create mode 100644 test/mv_web/components/layouts/sidebar_test.exs diff --git a/assets/css/app.css b/assets/css/app.css index ea63a2d..ba4c91e 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -99,4 +99,138 @@ /* Make LiveView wrapper divs transparent for layout */ [data-phx-session] { display: contents } +/* ============================================ + Sidebar Base Styles + ============================================ */ + +/* Desktop Sidebar Base */ +.sidebar { + @apply flex flex-col bg-base-200 min-h-screen; + @apply transition-[width] duration-300 ease-in-out; + @apply relative; + width: 16rem; /* Expanded: w-64 */ + z-index: 40; +} + +/* 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 .sidebar-expanded-icon { + @apply hidden; +} + +[data-sidebar-expanded="false"] .sidebar .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 - Center in Collapsed State + ============================================ */ + +[data-sidebar-expanded="false"] .sidebar .menu > li > a, +[data-sidebar-expanded="false"] .sidebar .menu > li > button { + @apply justify-center px-0; +} + +/* ============================================ + Footer Button Alignment - Center in Collapsed State + ============================================ */ + +[data-sidebar-expanded="false"] .sidebar .dropdown > button { + @apply justify-center px-0; +} + +/* ============================================ + Mobile Drawer Width + ============================================ */ + +/* Auf Mobile (< 1024px) ist die Sidebar immer w-64 (16rem) wenn geöffnet */ +@media (max-width: 1023px) { + .drawer-side .sidebar { + width: 16rem; /* w-64 auch auf Mobile */ + } +} + +/* ============================================ + Drawer Side Overflow Fix für Desktop + ============================================ */ + +/* Im Desktop-Modus (lg:drawer-open) overflow auf visible setzen + damit Dropdowns und Tooltips über Main Content erscheinen können */ +@media (min-width: 1024px) { + .drawer.lg\:drawer-open .drawer-side { + overflow: visible !important; + overflow-x: visible !important; + overflow-y: visible !important; + } +} + /* This file is for your main application CSS */ diff --git a/assets/js/app.js b/assets/js/app.js index 73e57fc..267ae05 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -73,6 +73,43 @@ Hooks.ComboBox = { } } +// SidebarState hook: Manages sidebar expanded/collapsed state +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) { + // Convert boolean to string for consistency + const expandedStr = expanded ? 'true' : 'false' + + // Update data-attribute (CSS reacts to this) + this.el.dataset.sidebarExpanded = expandedStr + + // Persist to localStorage + localStorage.setItem('sidebar-expanded', expandedStr) + + // Update ARIA for accessibility + const toggleBtn = document.getElementById('sidebar-toggle') + if (toggleBtn) { + toggleBtn.setAttribute('aria-expanded', expandedStr) + } + }, + + destroyed() { + // Cleanup + delete window.toggleSidebar + } +} + let liveSocket = new LiveSocket("/live", Socket, { longPollFallbackMs: 2500, params: {_csrf_token: csrfToken}, @@ -104,7 +141,7 @@ window.liveSocket = liveSocket // Sidebar accessibility improvements document.addEventListener("DOMContentLoaded", () => { - const drawerToggle = document.getElementById("main-drawer") + const drawerToggle = document.getElementById("mobile-drawer") const sidebarToggle = document.getElementById("sidebar-toggle") const sidebar = document.getElementById("main-sidebar") diff --git a/docs/daisyui-drawer-pattern.md b/docs/daisyui-drawer-pattern.md new file mode 100644 index 0000000..f6dc7e9 --- /dev/null +++ b/docs/daisyui-drawer-pattern.md @@ -0,0 +1,532 @@ +# DaisyUI Drawer Pattern - Standard Implementation + +This document describes the standard DaisyUI drawer pattern for implementing responsive sidebars. It covers mobile overlay drawers, desktop persistent sidebars, and their combination. + +## Core Concept + +DaisyUI's drawer component uses a **checkbox-based toggle mechanism** combined with CSS to create accessible, responsive sidebars without custom JavaScript. + +### Key Components + +1. **`drawer`** - Container element +2. **`drawer-toggle`** - Hidden checkbox that controls open/close state +3. **`drawer-content`** - Main content area +4. **`drawer-side`** - Sidebar content (menu, navigation) +5. **`drawer-overlay`** - Optional overlay for mobile (closes drawer on click) + +## HTML Structure + +```html +
+ + + + +
+ + +
+ + + +
+``` + +## How drawer-toggle Works + +### Mechanism + +The `drawer-toggle` is a **hidden checkbox** that serves as the state controller: + +```html + +``` + +### Toggle Behavior + +1. **Label Connection**: Any `