Some checks failed
continuous-integration/drone/push Build is failing
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
137 lines
4.4 KiB
Elixir
137 lines
4.4 KiB
Elixir
defmodule MvWeb.Layouts do
|
|
@moduledoc """
|
|
This module holds different layouts used by your application.
|
|
|
|
See the `layouts` directory for all templates available.
|
|
The "root" layout is a skeleton rendered as part of the
|
|
application router. The "app" layout is rendered as component
|
|
in regular views and live views.
|
|
"""
|
|
use MvWeb, :html
|
|
use Gettext, backend: MvWeb.Gettext
|
|
import MvWeb.Layouts.Sidebar
|
|
|
|
embed_templates "layouts/*"
|
|
|
|
@doc """
|
|
Renders the app layout. Can be used with or without a current_user.
|
|
When current_user is present, it will show the navigation bar.
|
|
|
|
## Examples
|
|
|
|
<Layouts.app flash={@flash}>
|
|
<h1>Content</h1>
|
|
</Layout.app>
|
|
|
|
<Layouts.app flash={@flash} current_user={@current_user}>
|
|
<h1>Authenticated Content</h1>
|
|
</Layout.app>
|
|
|
|
"""
|
|
attr :flash, :map, required: true, doc: "the map of flash messages"
|
|
|
|
attr :current_user, :map, default: nil, doc: "the current user, if authenticated"
|
|
|
|
attr :current_scope, :map,
|
|
default: nil,
|
|
doc: "the current [scope](https://hexdocs.pm/phoenix/scopes.html)"
|
|
|
|
slot :inner_block, required: true
|
|
|
|
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 relative z-0">
|
|
<!-- 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 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-40">
|
|
<.sidebar current_user={@current_user} club_name={@club_name} mobile={false} />
|
|
</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
|
|
|
|
# Helper function to get club name from settings
|
|
# Falls back to "Mitgliederverwaltung" if settings can't be loaded
|
|
defp get_club_name do
|
|
case Mv.Membership.get_settings() do
|
|
{:ok, settings} -> settings.club_name
|
|
_ -> "Mitgliederverwaltung"
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Shows the flash group with standard titles and content.
|
|
|
|
## Examples
|
|
|
|
<.flash_group flash={@flash} />
|
|
"""
|
|
attr :flash, :map, required: true, doc: "the map of flash messages"
|
|
attr :id, :string, default: "flash-group", doc: "the optional id of flash container"
|
|
|
|
def flash_group(assigns) do
|
|
~H"""
|
|
<div id={@id} aria-live="polite" class="z-50 flex flex-col gap-2 toast toast-top toast-end">
|
|
<.flash kind={:success} flash={@flash} />
|
|
<.flash kind={:warning} flash={@flash} />
|
|
<.flash kind={:info} flash={@flash} />
|
|
<.flash kind={:error} flash={@flash} />
|
|
|
|
<.flash
|
|
id="client-error"
|
|
kind={:error}
|
|
title={gettext("We can't find the internet")}
|
|
phx-disconnected={show(".phx-client-error #client-error") |> JS.remove_attribute("hidden")}
|
|
phx-connected={hide("#client-error") |> JS.set_attribute({"hidden", ""})}
|
|
hidden
|
|
>
|
|
{gettext("Attempting to reconnect")}
|
|
<.icon name="hero-arrow-path" class="ml-1 size-3 motion-safe:animate-spin" />
|
|
</.flash>
|
|
|
|
<.flash
|
|
id="server-error"
|
|
kind={:error}
|
|
title={gettext("Something went wrong!")}
|
|
phx-disconnected={show(".phx-server-error #server-error") |> JS.remove_attribute("hidden")}
|
|
phx-connected={hide("#server-error") |> JS.set_attribute({"hidden", ""})}
|
|
hidden
|
|
>
|
|
{gettext("Attempting to reconnect")}
|
|
<.icon name="hero-arrow-path" class="ml-1 size-3 motion-safe:animate-spin" />
|
|
</.flash>
|
|
</div>
|
|
"""
|
|
end
|
|
end
|