diff --git a/assets/css/app.css b/assets/css/app.css index 97961ab..ea63a2d 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -99,213 +99,4 @@ /* 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 */ -} - -/* ============================================ - Header - Logo Centering - ============================================ */ - -/* Header container with smooth transition for gap */ -.sidebar > div:first-child { - @apply transition-all duration-300; -} - -/* ============================================ - Text Labels - Hide in Collapsed State - ============================================ */ - -.menu-label { - @apply transition-all duration-200 whitespace-nowrap; - transition-delay: 0ms; /* Expanded: sofort sichtbar */ -} - -[data-sidebar-expanded="false"] .sidebar .menu-label { - @apply opacity-0 w-0 overflow-hidden pointer-events-none; - transition-delay: 300ms; /* Warte bis Sidebar eingeklappt ist (300ms = duration der Sidebar width transition) */ -} - -/* ============================================ - 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; -} - -/* Collapsed menu group button: center icon under logo */ -.sidebar .collapsed-menu-group button { - padding-left: 14px; -} - -/* ============================================ - 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 - Icons Centered Under Logo - ============================================ */ - -/* Base alignment: Icons centered under logo (32px from left edge) - - Logo center: 16px padding + 16px (half of 32px) = 32px - - Icon center should be at 32px: 22px start + 10px (half of 20px) = 32px - - Menu has p-2 (8px), so links need 14px additional padding-left */ - -.sidebar .menu > li > a, -.sidebar .menu > li > button { - @apply transition-all duration-300; - padding-left: 14px; -} - -/* Collapsed state: same padding to keep icons at same position - - Remove gap so label (which is opacity-0 w-0) doesn't create space - - Keep padding-left at 14px so icons stay centered under logo */ -[data-sidebar-expanded="false"] .sidebar .menu > li > a, -[data-sidebar-expanded="false"] .sidebar .menu > li > button { - @apply gap-0; - padding-left: 14px; - padding-right: 14px; /* Center icon horizontally in 64px sidebar */ -} - -/* ============================================ - Footer Button Alignment - Left Aligned in Collapsed State - ============================================ */ - -[data-sidebar-expanded="false"] .sidebar .dropdown > button { - @apply px-0; - /* Buttons stay at left position, only label disappears */ -} - -/* ============================================ - User Menu Button - Focus Ring on Avatar - ============================================ */ - -/* Focus ring appears on the avatar when button is focused */ -.user-menu-button:focus .avatar > div { - @apply ring-2 ring-primary ring-offset-2 ring-offset-base-200; -} - -/* ============================================ - User Menu Button - Smooth Centering Transition - ============================================ */ - -/* User menu button transitions smoothly to center */ -.user-menu-button { - @apply transition-all duration-300; -} - -/* In collapsed state, center avatar under logo - - Avatar is 32px (w-8), center it in 64px sidebar - - (64px - 32px) / 2 = 16px padding → avatar center at 32px (same as logo center) */ -[data-sidebar-expanded="false"] .sidebar .user-menu-button { - @apply gap-0; - padding-left: 16px; - padding-right: 16px; - justify-content: center; -} - -/* ============================================ - User Menu Button - Hover Ring on Avatar - ============================================ */ - -/* Smooth transition for avatar ring effects */ -.user-menu-button .avatar > div { - @apply transition-all duration-200; -} - -/* Hover ring appears on the avatar when button is hovered */ -.user-menu-button:hover .avatar > div { - @apply ring-1 ring-neutral ring-offset-1 ring-offset-base-200; -} - -/* ============================================ - 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 267ae05..883ca30 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -73,43 +73,6 @@ 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}, @@ -139,170 +102,3 @@ liveSocket.connect() // >> liveSocket.disableLatencySim() window.liveSocket = liveSocket -// Sidebar accessibility improvements -document.addEventListener("DOMContentLoaded", () => { - const drawerToggle = document.getElementById("mobile-drawer") - const sidebarToggle = document.getElementById("sidebar-toggle") - const sidebar = document.getElementById("main-sidebar") - - if (!drawerToggle || !sidebarToggle || !sidebar) return - - // Manage tabindex for sidebar elements based on open/closed state - const updateSidebarTabIndex = (isOpen) => { - // Find all potentially focusable elements (including those with tabindex="-1") - const allFocusableElements = sidebar.querySelectorAll( - 'a[href], button, select, input:not([type="hidden"]), [tabindex]' - ) - - allFocusableElements.forEach(el => { - // Skip the overlay button - if (el.closest('.drawer-overlay')) return - - if (isOpen) { - // Remove tabindex="-1" to make focusable when open - if (el.hasAttribute('tabindex') && el.getAttribute('tabindex') === '-1') { - el.removeAttribute('tabindex') - } - } else { - // Set tabindex="-1" to remove from tab order when closed - if (!el.hasAttribute('tabindex')) { - el.setAttribute('tabindex', '-1') - } else if (el.getAttribute('tabindex') !== '-1') { - // Store original tabindex in data attribute before setting to -1 - if (!el.hasAttribute('data-original-tabindex')) { - el.setAttribute('data-original-tabindex', el.getAttribute('tabindex')) - } - el.setAttribute('tabindex', '-1') - } - } - }) - } - - // Find first focusable element in sidebar - // Priority: first navigation link (menuitem) > other links > other focusable elements - const getFirstFocusableElement = () => { - // First, try to find the first navigation link (menuitem) - const firstNavLink = sidebar.querySelector('a[href][role="menuitem"]:not([tabindex="-1"])') - if (firstNavLink && !firstNavLink.closest('.drawer-overlay')) { - return firstNavLink - } - - // Fallback: any navigation link - const firstLink = sidebar.querySelector('a[href]:not([tabindex="-1"])') - if (firstLink && !firstLink.closest('.drawer-overlay')) { - return firstLink - } - - // Last resort: any other focusable element - const focusableSelectors = [ - 'button:not([tabindex="-1"]):not([disabled])', - 'select:not([tabindex="-1"]):not([disabled])', - 'input:not([tabindex="-1"]):not([disabled]):not([type="hidden"])', - '[tabindex]:not([tabindex="-1"])' - ] - - for (const selector of focusableSelectors) { - const element = sidebar.querySelector(selector) - if (element && !element.closest('.drawer-overlay')) { - return element - } - } - return null - } - - // Update aria-expanded when drawer state changes - const updateAriaExpanded = () => { - const isOpen = drawerToggle.checked - sidebarToggle.setAttribute("aria-expanded", isOpen.toString()) - - // Update dropdown aria-expanded if present - const userMenuButton = sidebar.querySelector('button[aria-haspopup="true"]') - if (userMenuButton) { - const dropdown = userMenuButton.closest('.dropdown') - const isDropdownOpen = dropdown?.classList.contains('dropdown-open') - if (userMenuButton) { - userMenuButton.setAttribute("aria-expanded", (isDropdownOpen || false).toString()) - } - } - } - - // Listen for changes to the drawer checkbox - drawerToggle.addEventListener("change", () => { - const isOpen = drawerToggle.checked - updateAriaExpanded() - updateSidebarTabIndex(isOpen) - if (!isOpen) { - // When closing, return focus to toggle button - sidebarToggle.focus() - } - }) - - // Update on initial load - updateAriaExpanded() - updateSidebarTabIndex(drawerToggle.checked) - - // Close sidebar with ESC key - document.addEventListener("keydown", (e) => { - if (e.key === "Escape" && drawerToggle.checked) { - drawerToggle.checked = false - updateAriaExpanded() - updateSidebarTabIndex(false) - // Return focus to toggle button - sidebarToggle.focus() - } - }) - - // Improve keyboard navigation for sidebar toggle - sidebarToggle.addEventListener("keydown", (e) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault() - const wasOpen = drawerToggle.checked - drawerToggle.checked = !drawerToggle.checked - updateAriaExpanded() - - // If opening, move focus to first element in sidebar - if (!wasOpen && drawerToggle.checked) { - updateSidebarTabIndex(true) - // Use setTimeout to ensure DOM is updated - setTimeout(() => { - const firstElement = getFirstFocusableElement() - if (firstElement) { - firstElement.focus() - } - }, 50) - } else if (wasOpen && !drawerToggle.checked) { - updateSidebarTabIndex(false) - } - } - }) - - // Also handle click events to update tabindex and focus - sidebarToggle.addEventListener("click", () => { - setTimeout(() => { - const isOpen = drawerToggle.checked - updateSidebarTabIndex(isOpen) - if (isOpen) { - const firstElement = getFirstFocusableElement() - if (firstElement) { - firstElement.focus() - } - } - }, 50) - }) - - // Handle dropdown keyboard navigation - const userMenuButton = sidebar?.querySelector('button[aria-haspopup="true"]') - if (userMenuButton) { - userMenuButton.addEventListener("click", () => { - setTimeout(updateAriaExpanded, 0) - }) - - userMenuButton.addEventListener("keydown", (e) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault() - userMenuButton.click() - } - }) - } -}) - diff --git a/docker-compose.yml b/docker-compose.yml index 4c169b5..8621603 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,7 +10,11 @@ services: POSTGRES_PASSWORD: postgres POSTGRES_DB: mv_dev volumes: - - postgres-data:/var/lib/postgresql/data + - type: volume + source: postgres-data + target: /var/lib/postgresql/data + volume: + nocopy: true ports: - "5000:5432" networks: @@ -45,7 +49,9 @@ services: - rauthy-dev - local volumes: - - rauthy-data:/app/data + - type: volume + source: rauthy-data + target: /app/data volumes: postgres-data: diff --git a/docs/csv-member-import-v1.md b/docs/csv-member-import-v1.md index bc8f99f..2bdbe69 100644 --- a/docs/csv-member-import-v1.md +++ b/docs/csv-member-import-v1.md @@ -191,26 +191,6 @@ A **basic CSV member import feature** that allows administrators to upload a CSV - `/templates/member_import_de.csv` - In LiveView, link them using Phoenix static path helpers (e.g. `~p` or `Routes.static_path/2`, depending on Phoenix version). -**Example Usage in LiveView Templates:** - -```heex - -<.link href={~p"/templates/member_import_en.csv"} download> - <%= gettext("Download English Template") %> - - -<.link href={~p"/templates/member_import_de.csv"} download> - <%= gettext("Download German Template") %> - - - -<.link href={Routes.static_path(MvWeb.Endpoint, "/templates/member_import_en.csv")} download> - <%= gettext("Download English Template") %> - -``` - -**Note:** The `templates` directory must be included in `MvWeb.static_paths()` (configured in `lib/mv_web.ex`) for the files to be served. - ### File Limits - **Max file size:** 10 MB diff --git a/docs/daisyui-drawer-pattern.md b/docs/daisyui-drawer-pattern.md deleted file mode 100644 index dec599d..0000000 --- a/docs/daisyui-drawer-pattern.md +++ /dev/null @@ -1,533 +0,0 @@ -# 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 -