From 104faf70067bf4888c31c00b738dffe401b846b0 Mon Sep 17 00:00:00 2001 From: Simon Date: Fri, 13 Mar 2026 14:48:10 +0100 Subject: [PATCH] feat: add theme selector to unauthenticated pages --- DESIGN_GUIDELINES.md | 12 +++--- assets/css/app.css | 8 ++++ lib/mv_web/components/core_components.ex | 35 +++++++++++++++ lib/mv_web/components/layouts.ex | 54 +++++++++++++----------- lib/mv_web/components/layouts/sidebar.ex | 54 +++++++----------------- lib/mv_web/live/join_live.ex | 4 +- 6 files changed, 98 insertions(+), 69 deletions(-) diff --git a/DESIGN_GUIDELINES.md b/DESIGN_GUIDELINES.md index 6e8ca40..187864c 100644 --- a/DESIGN_GUIDELINES.md +++ b/DESIGN_GUIDELINES.md @@ -81,7 +81,7 @@ If the `<.header>` is outside the `<.form>`, the submit button must reference th Pages that do not require authentication (e.g. `/join`, `/sign-in`, `/confirm_join/:token`) use a unified layout via the **`Layouts.public_page`** component: - **Component:** `Layouts.public_page` renders: - - **Header:** Logo + "Mitgliederverwaltung" (left) | Club name centered via absolute positioning | Language selector (right) + - **Header:** Logo + "Mitgliederverwaltung" (left) | Club name centered via absolute positioning | Language selector + theme swap (sun/moon, DaisyUI swap with rotate) (right) - Main content slot, Flash group. No sidebar, no authenticated-layout logic. - **Content:** DaisyUI **hero** section (`hero`, `hero-content`) for the main message or form, so all public pages share the same visual structure. The hero is constrained in width (`max-w-4xl mx-auto`) and content is left-aligned (`hero-content flex-col items-start text-left`). - **Locale handling:** The language selector uses `Gettext.get_locale(MvWeb.Gettext)` (backend-specific) to correctly reflect the active locale. `SignInLive` sets both `Gettext.put_locale(MvWeb.Gettext, locale)` and `Gettext.put_locale(locale)` to keep global and backend locales in sync. @@ -98,16 +98,18 @@ Use these standard roles: | Role | Use | Class | |---|---|---| | Page title (H1) | main page title | `text-xl font-semibold leading-8` | -| Subtitle | helper under title | `text-sm text-base-content/70` | +| Subtitle | helper under title | `text-sm text-base-content/85` | | Section title (H2) | section headings | `text-lg font-semibold` | -| Helper text | under inputs | `text-sm text-base-content/70` | -| Fine print | small hints | `text-xs text-base-content/60` | -| Empty state | no data | `text-base-content/60 italic` | +| Helper text | under inputs | `text-sm text-base-content/85` | +| Fine print | small hints | `text-xs text-base-content/80` | +| Empty state | no data | `text-base-content/80 italic` | | Destructive text | danger | `text-error` | **MUST:** Page titles via `<.header>`. **MUST:** Section titles via `<.form_section title="…">` (for forms) or a consistent section wrapper (if you introduce a `<.card>` later). +**Form labels (WCAG 2.2 AA):** DaisyUI `.label` defaults to 60% opacity and fails contrast. We override it in `app.css` to 85% of `base-content` so labels stay slightly de‑emphasised vs body text but meet the 4.5:1 minimum. Use `class="label"` and `` as usual; no extra classes needed. + --- ## 4) States: Loading, Empty, Error (mandatory consistency) diff --git a/assets/css/app.css b/assets/css/app.css index e3c6e83..e79b4b6 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -154,6 +154,14 @@ background-color: var(--color-base-100); } +/* WCAG 2.2 AA (4.5:1 for normal text): Form labels. DaisyUI .label uses 60% opacity, + which fails contrast. Override to 85% of base-content so labels stay slightly + de‑emphasised vs body text but meet the minimum ratio. */ +[data-theme="light"] .label, +[data-theme="dark"] .label { + color: color-mix(in oklab, var(--color-base-content) 85%, transparent); +} + /* WCAG 2.2 AA (4.5:1 for normal text): Badge text must contrast with badge background. Theme tokens *-content are often too light on * backgrounds in light theme, and badge-soft uses variant as text on a light tint (low contrast). We override diff --git a/lib/mv_web/components/core_components.ex b/lib/mv_web/components/core_components.ex index 11a60ef..8c58c32 100644 --- a/lib/mv_web/components/core_components.ex +++ b/lib/mv_web/components/core_components.ex @@ -1295,6 +1295,41 @@ defmodule MvWeb.CoreComponents do """ end + @doc """ + Renders a theme toggle using DaisyUI swap (sun/moon with rotate effect). + + Wired to the theme script in root layout: checkbox uses `data-theme-toggle`, + root script syncs checked state (checked = dark) and listens for `phx:set-theme`. + Use in public header or sidebar. Optional `class` is applied to the wrapper. + """ + attr :class, :string, default: nil, doc: "Optional extra classes for the swap wrapper" + + def theme_swap(assigns) do + assigns = assign(assigns, :wrapper_class, assigns[:class]) + + ~H""" +
+ +
+ """ + end + @doc """ Renders a [Heroicon](https://heroicons.com). diff --git a/lib/mv_web/components/layouts.ex b/lib/mv_web/components/layouts.ex index 22408c7..5258ab9 100644 --- a/lib/mv_web/components/layouts.ex +++ b/lib/mv_web/components/layouts.ex @@ -39,18 +39,21 @@ defmodule MvWeb.Layouts do {@club_name} -
- - -
+
+
+ + +
+ <.theme_swap /> +
@@ -156,18 +159,21 @@ defmodule MvWeb.Layouts do {@club_name} -
- - -
+
+
+ + +
+ <.theme_swap /> +
diff --git a/lib/mv_web/components/layouts/sidebar.ex b/lib/mv_web/components/layouts/sidebar.ex index 4a90543..2a4ea98 100644 --- a/lib/mv_web/components/layouts/sidebar.ex +++ b/lib/mv_web/components/layouts/sidebar.ex @@ -251,21 +251,22 @@ defmodule MvWeb.Layouts.Sidebar do defp sidebar_footer(assigns) do ~H"""
- -
- - -
- - <.theme_toggle /> + +
+ <.theme_swap /> +
+ + +
+
<%= if @current_user do %> <.user_menu current_user={@current_user} /> @@ -274,29 +275,6 @@ defmodule MvWeb.Layouts.Sidebar do """ end - defp theme_toggle(assigns) do - ~H""" - - """ - end - attr :current_user, :map, default: nil, doc: "The current user" defp user_menu(assigns) do diff --git a/lib/mv_web/live/join_live.ex b/lib/mv_web/live/join_live.ex index 4716cf8..e83031c 100644 --- a/lib/mv_web/live/join_live.ex +++ b/lib/mv_web/live/join_live.ex @@ -100,13 +100,13 @@ defmodule MvWeb.JoinLive do />
-

+

{gettext( "By submitting your application you will receive an email with a confirmation link. Once you have confirmed your email address, your application will be reviewed." )}

-

+

{gettext( "Your details are only used to process your membership application and to contact you. To prevent abuse we also process technical data (e.g. IP address) only as necessary." )}