diff --git a/DESIGN_GUIDELINES.md b/DESIGN_GUIDELINES.md index 92f7a90..0a10a70 100644 --- a/DESIGN_GUIDELINES.md +++ b/DESIGN_GUIDELINES.md @@ -46,14 +46,14 @@ Every authenticated page should follow the same structure: **MUST:** Use `<.header>` on every page (except login/public pages). **SHOULD:** Put short explanations into `<:subtitle>` rather than sprinkling random text blocks. -### 2.2 Edit/New form header: Back button left (mandatory) +### 2.2 Edit/New form header and footer buttons (mandatory) For LiveViews that render an edit or new form (e.g. member, group, role, user, custom field, membership fee type): -- **MUST:** Provide a **Back** button on the **left** side of the header using the `<:leading>` slot (same as data fields: Back left, title next, primary action on the right). +- **MUST:** Provide a **Back** button on the **left** side of the header using the `<:leading>` slot (same as data fields: Back left, title next). - **MUST:** Use the same pattern everywhere: Back button with `variant="neutral"`, arrow-left icon, and label “Back”. It navigates to the previous context (e.g. detail page or index) via a `return_path`-style helper. -- **SHOULD:** Place the primary action (e.g. “Save”) in `<:actions>` on the right. -- **Rationale:** Users expect a consistent way to leave the form without submitting; Back left matches the data fields edit view and keeps primary actions on the right. +- **MUST:** Place **exactly one** form button bar **below all form fields**, inside the `<.form>`, with: **Abbrechen** (Cancel) left, **Speichern** (Save) right. Use `gettext("Cancel")`, `gettext("Save ")`, `phx-disable-with={gettext("Saving...")}` on the submit button. No submit button in the header; no duplicate submit buttons. +- **Rationale:** Users expect a consistent way to leave the form without submitting; Back left. One primary action (Save) per form, in the footer, avoids double submits and matches the reference (member edit form). **Template for form pages:** ```heex @@ -66,15 +66,20 @@ For LiveViews that render an edit or new form (e.g. member, group, role, user, c Page title (e.g. “Edit Member” or “New User”) <:subtitle>Short explanation. - <:actions> - <.button phx-disable-with={gettext("Saving...")} variant="primary" type="submit"> - {gettext("Save")} - - -``` -If the `<.header>` is outside the `<.form>`, the submit button must reference the form via the `form` attribute (e.g. `form="user-form"`). +<.form for={@form} id="..." phx-change="validate" phx-submit="save"> + <%!-- form sections and fields --%> +
+ <.button navigate={return_path(@return_to, @resource)} variant="neutral" type="button"> + {gettext("Abbrechen")} + + <.button type="submit" phx-disable-with={gettext("Speichern...")} variant="primary"> + {gettext("Speichern")} + +
+ +``` ## 3) Typography (system) diff --git a/assets/css/app.css b/assets/css/app.css index e3c6e83..4118f09 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -577,7 +577,9 @@ } /* ============================================ - WCAG 2.2 AA: Tab list inactive tab text contrast (4.5:1) + Member detail tabs (show + edit): inactive vs active contrast + WCAG 2.2 AA: inactive tab text contrast (4.5:1) + Active tab: visible border (DaisyUI tabs-bordered) and weight so which tab is selected is clear. ============================================ */ #member-tablist .tab:not(.tab-active) { color: oklch(0.35 0.02 285); @@ -586,6 +588,13 @@ color: oklch(0.72 0.02 257); } +/* Active tab: stronger underline (DaisyUI --tab-border-color) and font weight */ +#member-tablist .tab.tab-active, +#member-tablist .tab[aria-selected="true"] { + --tab-border-color: var(--color-base-content); + font-weight: 600; +} + /* ============================================ WCAG 2.2 AA: Link contrast - primary and accent ============================================ */ diff --git a/lib/mv_web/live/components/field_visibility_dropdown_component.ex b/lib/mv_web/live/components/field_visibility_dropdown_component.ex index 58777da..f31d914 100644 --- a/lib/mv_web/live/components/field_visibility_dropdown_component.ex +++ b/lib/mv_web/live/components/field_visibility_dropdown_component.ex @@ -70,6 +70,7 @@ defmodule MvWeb.Components.FieldVisibilityDropdownComponent do id="field-visibility-menu" icon="hero-adjustments-horizontal" button_label={gettext("Show/Hide Columns")} + button_class="btn-secondary" items={@all_items} checkboxes={true} selected={@selected_fields} diff --git a/lib/mv_web/live/custom_field_live/form_component.ex b/lib/mv_web/live/custom_field_live/form_component.ex index 2e98aeb..67e407b 100644 --- a/lib/mv_web/live/custom_field_live/form_component.ex +++ b/lib/mv_web/live/custom_field_live/form_component.ex @@ -19,7 +19,7 @@ defmodule MvWeb.CustomFieldLive.FormComponent do @impl true def render(assigns) do ~H""" -
+
<.button @@ -98,6 +98,20 @@ defmodule MvWeb.CustomFieldLive.FormComponent do label={gettext("Show in overview")} /> + <%!-- Buttons: below all form fields, above Danger zone --%> +
+ <.button type="button" variant="neutral" phx-click="cancel" phx-target={@myself}> + {gettext("Cancel")} + + <.button + type="submit" + phx-disable-with={gettext("Saving...")} + variant="primary" + > + {gettext("Save Data Field")} + +
+ <%= if @custom_field do %> <%!-- Danger zone: canonical pattern (same as member form) --%>
@@ -125,15 +139,6 @@ defmodule MvWeb.CustomFieldLive.FormComponent do
<% end %> - -
- <.button type="button" variant="neutral" phx-click="cancel" phx-target={@myself}> - {gettext("Cancel")} - - <.button phx-disable-with={gettext("Saving...")} variant="primary"> - {gettext("Save Data Field")} - -
diff --git a/lib/mv_web/live/group_live/form.ex b/lib/mv_web/live/group_live/form.ex index 2e79a7f..d47ebff 100644 --- a/lib/mv_web/live/group_live/form.ex +++ b/lib/mv_web/live/group_live/form.ex @@ -77,7 +77,7 @@ defmodule MvWeb.GroupLive.Form do def render(assigns) do ~H""" - <.form for={@form} id="group-form" phx-change="validate" phx-submit="save"> + <.form class="max-w-2xl" for={@form} id="group-form" phx-change="validate" phx-submit="save"> <.header> <:leading> <.button navigate={return_path(@return_to, @group)} variant="neutral"> @@ -86,11 +86,6 @@ defmodule MvWeb.GroupLive.Form do {@page_title} - <:actions> - <.button phx-disable-with={gettext("Saving...")} variant="primary" type="submit"> - {gettext("Save")} - -
@@ -104,6 +99,20 @@ defmodule MvWeb.GroupLive.Form do />
+ <%!-- Buttons: below all form fields, Abbrechen left (secondary), Speichern right (primary) --%> +
+ <.button navigate={return_path(@return_to, @group)} variant="neutral" type="button"> + {gettext("Cancel")} + + <.button + type="submit" + phx-disable-with={gettext("Saving...")} + variant="primary" + > + {gettext("Save group")} + +
+ <%!-- Danger zone: canonical pattern (same as member form) --%> <%= if @group && can?(@current_user, :destroy, @group) do %>
diff --git a/lib/mv_web/live/group_live/show.ex b/lib/mv_web/live/group_live/show.ex index 80c0df5..d65392d 100644 --- a/lib/mv_web/live/group_live/show.ex +++ b/lib/mv_web/live/group_live/show.ex @@ -118,259 +118,261 @@ defmodule MvWeb.GroupLive.Show do
- <%!-- Group Information --%> -
-
-

{gettext("Description")}

-
- <%= if @group.description && String.trim(@group.description) != "" do %> -

{@group.description}

- <% else %> -

{gettext("No description")}

- <% end %> +
+ <%!-- Group Information --%> +
+
+

{gettext("Description")}

+
+ <%= if @group.description && String.trim(@group.description) != "" do %> +

{@group.description}

+ <% else %> +

{gettext("No description")}

+ <% end %> +
-
-
-

{gettext("Members")}

-
-

- {ngettext( - "Total: %{count} member", - "Total: %{count} members", - @group.member_count || 0, - count: @group.member_count || 0 - )} -

- - <%= if can?(@current_user, :update, @group) do %> -
- <%= if assigns[:show_add_member_input] do %> -
-
-
-
- <%= for member <- @selected_members do %> - <.badge - variant="primary" - style="outline" - class="flex items-center gap-1" - > - {MvWeb.Helpers.MemberHelpers.display_name(member)} - <.tooltip content={gettext("Remove")} position="top"> - <.button - type="button" - variant="icon" - size="sm" - phx-click="remove_selected_member" - phx-value-member_id={member.id} - aria-label={ - gettext("Remove %{name}", - name: MvWeb.Helpers.MemberHelpers.display_name(member) - ) - } - class="p-0 h-4 w-4 min-h-0" - > - <.icon name="hero-x-mark" class="size-3" /> - - - - <% end %> - -
- - <%= if length(@available_members) > 0 do %> -
- <%= for {member, index} <- Enum.with_index(@available_members) do %> -
-

- {MvWeb.Helpers.MemberHelpers.display_name(member)} -

-

- {member.email || gettext("No email")} -

-
- <% end %> -
- <% end %> -
-
- <.button - type="button" - variant="primary" - phx-click="add_selected_members" - data-testid="group-show-add-selected-members-btn" - disabled={Enum.empty?(@selected_member_ids)} - aria-label={gettext("Add members")} - class="join-item" - > - <.icon name="hero-plus" class="size-5" /> - - <.button - type="button" - variant="neutral" - phx-click="hide_add_member_input" - aria-label={gettext("Cancel")} - class="join-item" - > - {gettext("Cancel")} - -
- <% else %> - <.button - variant="primary" - phx-click="show_add_member_input" - aria-label={gettext("Add Member")} - > - {gettext("Add Member")} - - <% end %> -
- <% end %> - - <%= if Enum.empty?(@group.members || []) do %> -

- {gettext("No members in this group")} +

+

{gettext("Members")}

+
+

+ {ngettext( + "Total: %{count} member", + "Total: %{count} members", + @group.member_count || 0, + count: @group.member_count || 0 + )}

- <% else %> -
- - - - - - <%= if can?(@current_user, :update, @group) do %> - - <% end %> - - - - <%= for member <- @group.members do %> - - - - <%= if can?(@current_user, :update, @group) do %> - + {MvWeb.Helpers.MemberHelpers.display_name(member)} + <.tooltip content={gettext("Remove")} position="top"> + <.button + type="button" + variant="icon" + size="sm" + phx-click="remove_selected_member" + phx-value-member_id={member.id} + aria-label={ + gettext("Remove %{name}", + name: MvWeb.Helpers.MemberHelpers.display_name(member) + ) + } + class="p-0 h-4 w-4 min-h-0" + > + <.icon name="hero-x-mark" class="size-3" /> + + + + <% end %> + + + + <%= if length(@available_members) > 0 do %> +
+ <%= for {member, index} <- Enum.with_index(@available_members) do %> +
+

+ {MvWeb.Helpers.MemberHelpers.display_name(member)} +

+

+ {member.email || gettext("No email")} +

+
+ <% end %> +
+ <% end %> + + + <.button + type="button" + variant="primary" + phx-click="add_selected_members" + data-testid="group-show-add-selected-members-btn" + disabled={Enum.empty?(@selected_member_ids)} + aria-label={gettext("Add members")} + class="join-item" + > + <.icon name="hero-plus" class="size-5" /> + + <.button + type="button" + variant="neutral" + phx-click="hide_add_member_input" + aria-label={gettext("Cancel")} + class="join-item" + > + {gettext("Cancel")} + + + <% else %> + <.button + variant="primary" + phx-click="show_add_member_input" + aria-label={gettext("Add Member")} + > + {gettext("Add Member")} + + <% end %> + + <% end %> + + <%= if Enum.empty?(@group.members || []) do %> +

+ {gettext("No members in this group")} +

+ <% else %> +
+
{gettext("Name")}{gettext("Email")}{gettext("Actions")}
- <.link - navigate={~p"/members/#{member.id}"} - class="link link-primary" - > - {MvWeb.Helpers.MemberHelpers.display_name(member)} - - - <.maybe_value value={member.email} empty_sr_text={gettext("No email")}> - - {member.email} - - - - <.tooltip content={gettext("Remove")} position="left"> - <.button - type="button" - variant="danger" - size="sm" - phx-click="remove_member" - phx-value-member_id={member.id} - data-testid="group-show-remove-member" - aria-label={gettext("Remove member from group")} + + <%= if can?(@current_user, :update, @group) do %> +
+ <%= if assigns[:show_add_member_input] do %> +
+
+
+
+ <%= for member <- @selected_members do %> + <.badge + variant="primary" + style="outline" + class="flex items-center gap-1" > - <.icon name="hero-trash" class="size-4" /> - - -
+ + + + + <%= if can?(@current_user, :update, @group) do %> + <% end %> - <% end %> - -
{gettext("Name")}{gettext("Email")}{gettext("Actions")}
-
- <% end %> + + + <%= for member <- @group.members do %> + + + <.link + navigate={~p"/members/#{member.id}"} + class="link link-primary" + > + {MvWeb.Helpers.MemberHelpers.display_name(member)} + + + + <.maybe_value value={member.email} empty_sr_text={gettext("No email")}> + + {member.email} + + + + <%= if can?(@current_user, :update, @group) do %> + + <.tooltip content={gettext("Remove")} position="left"> + <.button + type="button" + variant="danger" + size="sm" + phx-click="remove_member" + phx-value-member_id={member.id} + data-testid="group-show-remove-member" + aria-label={gettext("Remove member from group")} + > + <.icon name="hero-trash" class="size-4" /> + + + + <% end %> + + <% end %> + + +
+ <% end %> +
-
- <%!-- Danger zone: canonical pattern (same as member show) --%> - <%= if can?(@current_user, :destroy, @group) do %> -
-

- {gettext("Danger zone")} -

-
-

- {gettext( - "Deleting this group cannot be undone. All member-group associations will be permanently removed." - )} -

- <.button - id="delete-group-trigger" - variant="danger" - type="button" - phx-click="open_delete_modal" - data-testid="group-show-delete-btn" - aria-label={gettext("Delete group %{name}", name: @group.name)} - > - <.icon name="hero-trash" class="size-4" /> - {gettext("Delete group")} - -
-
- <% end %> + <%!-- Danger zone: same width as form (max-w-2xl) --%> + <%= if can?(@current_user, :destroy, @group) do %> +
+

+ {gettext("Danger zone")} +

+
+

+ {gettext( + "Deleting this group cannot be undone. All member-group associations will be permanently removed." + )} +

+ <.button + id="delete-group-trigger" + variant="danger" + type="button" + phx-click="open_delete_modal" + data-testid="group-show-delete-btn" + aria-label={gettext("Delete group %{name}", name: @group.name)} + > + <.icon name="hero-trash" class="size-4" /> + {gettext("Delete group")} + +
+
+ <% end %> +
<%!-- Delete Confirmation Modal (WCAG: focus in modal, aria-labelledby) --%> <%= if assigns[:show_delete_modal] do %> diff --git a/lib/mv_web/live/member_field_live/form_component.ex b/lib/mv_web/live/member_field_live/form_component.ex index 84889e5..400c777 100644 --- a/lib/mv_web/live/member_field_live/form_component.ex +++ b/lib/mv_web/live/member_field_live/form_component.ex @@ -166,12 +166,17 @@ defmodule MvWeb.MemberFieldLive.FormComponent do />
-
+ <%!-- Buttons: below all form fields, Cancel left (secondary), Speichern right (primary) --%> +
<.button type="button" variant="neutral" phx-click="cancel" phx-target={@myself}> {gettext("Cancel")} - <.button phx-disable-with={gettext("Saving...")} variant="primary"> - {gettext("Save Field")} + <.button + type="submit" + phx-disable-with={gettext("Saving...")} + variant="primary" + > + {gettext("Save datafield")}
diff --git a/lib/mv_web/live/member_live/form.ex b/lib/mv_web/live/member_live/form.ex index 6d187fa..bc7e7d1 100644 --- a/lib/mv_web/live/member_live/form.ex +++ b/lib/mv_web/live/member_live/form.ex @@ -63,6 +63,7 @@ defmodule MvWeb.MemberLive.Form do
<%!-- Tab navigation: same styling as member show; only Contact Data tab (no Membership Fees on edit) --%>
@@ -259,13 +260,17 @@ defmodule MvWeb.MemberLive.Form do
- <%!-- Bottom Action Buttons --%> -
- <.button navigate={return_path(@return_to, @member)} variant="neutral" type="button"> + <%!-- Buttons: below all form fields, Cancel left (secondary), Speichern right (primary) --%> +
+ <.link navigate={return_path(@return_to, @member)} class="btn btn-secondary"> {gettext("Cancel")} - - <.button phx-disable-with={gettext("Saving...")} variant="primary" type="submit"> - {gettext("Save Member")} + + <.button + type="submit" + phx-disable-with={gettext("Saving...")} + class="btn btn-primary" + > + {gettext("Save member")}
diff --git a/lib/mv_web/live/member_live/index.html.heex b/lib/mv_web/live/member_live/index.html.heex index b35d426..c9e8960 100644 --- a/lib/mv_web/live/member_live/index.html.heex +++ b/lib/mv_web/live/member_live/index.html.heex @@ -44,6 +44,13 @@ query={@query} placeholder={gettext("Search...")} /> + <.live_component + module={MvWeb.Components.FieldVisibilityDropdownComponent} + id="field-visibility-dropdown" + all_fields={@all_available_fields} + custom_fields={@all_custom_fields} + selected_fields={@user_field_selection} + /> <.live_component module={MvWeb.Components.MemberFilterComponent} id="member-filter" @@ -86,13 +93,6 @@ - <.live_component - module={MvWeb.Components.FieldVisibilityDropdownComponent} - id="field-visibility-dropdown" - all_fields={@all_available_fields} - custom_fields={@all_custom_fields} - selected_fields={@user_field_selection} - />
<%!-- On desktop (lg:), only the table area scrolls; header and filters stay visible. On mobile, normal flow. --%> diff --git a/lib/mv_web/live/membership_fee_settings_live.ex b/lib/mv_web/live/membership_fee_settings_live.ex index f95fa8a..694aad4 100644 --- a/lib/mv_web/live/membership_fee_settings_live.ex +++ b/lib/mv_web/live/membership_fee_settings_live.ex @@ -144,11 +144,6 @@ defmodule MvWeb.MembershipFeeSettingsLive do <:subtitle> {gettext("Configure fee types for membership fees.")} - <:actions> - <.button variant="primary" navigate={~p"/membership_fee_settings/new_fee_type"}> - <.icon name="hero-plus" /> {gettext("New Membership Fee Type")} - - <%!-- One card: default setting + fee types table --%> @@ -220,13 +215,6 @@ defmodule MvWeb.MembershipFeeSettingsLive do <% end %> <% end %> - -
- <.button type="submit" variant="primary"> - <.icon name="hero-check" class="size-5" /> - {gettext("Save Settings")} - -
    @@ -237,12 +225,24 @@ defmodule MvWeb.MembershipFeeSettingsLive do )}
+ + <%!-- Save button: below default settings form, no icon (consistent with other Save buttons) --%> +
+ <.button type="submit" phx-disable-with={gettext("Saving...")} variant="primary"> + {gettext("Save default settings")} + +
- <%!-- Fee types table: row click opens edit --%> -

{gettext("Membership Fee Types")}

+ <%!-- Fee types section: heading and "New" button on same line --%> +
+

{gettext("Membership Fee Types")}

+ <.button variant="primary" navigate={~p"/membership_fee_settings/new_fee_type"}> + <.icon name="hero-plus" /> {gettext("New Membership Fee Type")} + +
<.table id="membership_fee_types" rows={@membership_fee_types} diff --git a/lib/mv_web/live/membership_fee_type_live/form.ex b/lib/mv_web/live/membership_fee_type_live/form.ex index 215bc2f..60ff9aa 100644 --- a/lib/mv_web/live/membership_fee_type_live/form.ex +++ b/lib/mv_web/live/membership_fee_type_live/form.ex @@ -34,16 +34,6 @@ defmodule MvWeb.MembershipFeeTypeLive.Form do {@page_title} - <:actions> - <.button - form="membership-fee-type-form" - phx-disable-with={gettext("Saving...")} - variant="primary" - type="submit" - > - {gettext("Save")} - - <.form @@ -139,13 +129,22 @@ defmodule MvWeb.MembershipFeeTypeLive.Form do rows="3" /> -
- <.button phx-disable-with={gettext("Saving...")} variant="primary" type="submit"> - {gettext("Save Membership Fee Type")} - - <.button navigate={return_path(@return_to, @membership_fee_type)} type="button"> + <%!-- Buttons: below all form fields, Cancel left (secondary), Speichern right (primary) --%> +
+ <.button + navigate={return_path(@return_to, @membership_fee_type)} + variant="neutral" + type="button" + > {gettext("Cancel")} + <.button + type="submit" + phx-disable-with={gettext("Saving...")} + variant="primary" + > + {gettext("Save membership fee type")} +
diff --git a/lib/mv_web/live/role_live/form.ex b/lib/mv_web/live/role_live/form.ex index f3b1663..fd783cc 100644 --- a/lib/mv_web/live/role_live/form.ex +++ b/lib/mv_web/live/role_live/form.ex @@ -30,11 +30,6 @@ defmodule MvWeb.RoleLive.Form do {@page_title} - <:actions> - <.button phx-disable-with={gettext("Saving...")} variant="primary" type="submit"> - {gettext("Save")} - -
@@ -85,6 +80,20 @@ defmodule MvWeb.RoleLive.Form do <% end %>
+ + <%!-- Buttons: below all form fields, Cancel left (secondary), Speichern right (primary) --%> +
+ <.button navigate={return_path(@return_to, @role)} variant="neutral" type="button"> + {gettext("Cancel")} + + <.button + type="submit" + phx-disable-with={gettext("Saving...")} + variant="primary" + > + {gettext("Save role")} + +
""" diff --git a/lib/mv_web/live/role_live/show.ex b/lib/mv_web/live/role_live/show.ex index a5402c4..5553d93 100644 --- a/lib/mv_web/live/role_live/show.ex +++ b/lib/mv_web/live/role_live/show.ex @@ -223,55 +223,57 @@ defmodule MvWeb.RoleLive.Show do phx-hook="FocusRestore" phx-window-keydown={if @show_delete_modal, do: "window_keydown", else: nil} > - <.list> - <:item title={gettext("Name")}>{@role.name} - <:item title={gettext("Description")}> - <%= if @role.description do %> - {@role.description} - <% else %> - {gettext("No description")} - <% end %> - - <:item title={gettext("Permission Set")}> - <.badge variant={permission_set_badge_variant(@role.permission_set_name)}> - {@role.permission_set_name} - - - <:item title={gettext("System Role")}> - <.badge :if={@role.is_system_role} variant="warning"> - {gettext("Yes")} - - <.badge :if={!@role.is_system_role} variant="neutral"> - {gettext("No")} - - - +
+ <.list> + <:item title={gettext("Name")}>{@role.name} + <:item title={gettext("Description")}> + <%= if @role.description do %> + {@role.description} + <% else %> + {gettext("No description")} + <% end %> + + <:item title={gettext("Permission Set")}> + <.badge variant={permission_set_badge_variant(@role.permission_set_name)}> + {@role.permission_set_name} + + + <:item title={gettext("System Role")}> + <.badge :if={@role.is_system_role} variant="warning"> + {gettext("Yes")} + + <.badge :if={!@role.is_system_role} variant="neutral"> + {gettext("No")} + + + - <%!-- Danger zone: canonical pattern (same as member show) --%> - <%= if can?(@current_user, :destroy, Mv.Authorization.Role) and not @role.is_system_role do %> -
-

- {gettext("Danger zone")} -

-
-

- {gettext( - "Deleting this role cannot be undone. Users assigned to this role must be reassigned first." - )} -

- <.button - id="delete-role-trigger" - variant="danger" - phx-click="open_delete_modal" - data-testid="role-delete" - aria-label={gettext("Delete role %{name}", name: @role.name)} - > - <.icon name="hero-trash" class="size-4" /> - {gettext("Delete role")} - -
-
- <% end %> + <%!-- Danger zone: canonical pattern (same as member show) --%> + <%= if can?(@current_user, :destroy, Mv.Authorization.Role) and not @role.is_system_role do %> +
+

+ {gettext("Danger zone")} +

+
+

+ {gettext( + "Deleting this role cannot be undone. Users assigned to this role must be reassigned first." + )} +

+ <.button + id="delete-role-trigger" + variant="danger" + phx-click="open_delete_modal" + data-testid="role-delete" + aria-label={gettext("Delete role %{name}", name: @role.name)} + > + <.icon name="hero-trash" class="size-4" /> + {gettext("Delete role")} + +
+
+ <% end %> +
<%!-- Delete Role Confirmation Modal (WCAG: focus moves into modal, keyboard confirm/cancel) --%> <%= if assigns[:show_delete_modal] do %> diff --git a/lib/mv_web/live/user_live/form.ex b/lib/mv_web/live/user_live/form.ex index 5232ec7..b910353 100644 --- a/lib/mv_web/live/user_live/form.ex +++ b/lib/mv_web/live/user_live/form.ex @@ -60,16 +60,6 @@ defmodule MvWeb.UserLive.Form do {@page_title} - <:actions> - <.button - form="user-form" - phx-disable-with={gettext("Saving...")} - variant="primary" - type="submit" - > - {gettext("Save User")} - -
<% end %> + <%!-- Buttons: below all form fields, above Danger zone --%> +
+ <.button navigate={return_path(@return_to, @user)} variant="neutral" type="button"> + {gettext("Cancel")} + + <.button + type="submit" + phx-disable-with={gettext("Saving...")} + variant="primary" + > + {gettext("Save user")} + +
+ <%!-- Danger zone: canonical pattern (same as member form) --%> <%= if @user && can?(@current_user, :destroy, @user) && !SystemActor.system_user?(@user) do %>
@@ -378,15 +382,6 @@ defmodule MvWeb.UserLive.Form do
<% end %> - -
- <.button navigate={return_path(@return_to, @user)} variant="neutral"> - {gettext("Cancel")} - - <.button phx-disable-with={gettext("Saving...")} variant="primary"> - {gettext("Save User")} - -
diff --git a/lib/mv_web/live/user_live/show.ex b/lib/mv_web/live/user_live/show.ex index 72070d8..56c0549 100644 --- a/lib/mv_web/live/user_live/show.ex +++ b/lib/mv_web/live/user_live/show.ex @@ -67,59 +67,61 @@ defmodule MvWeb.UserLive.Show do phx-hook="FocusRestore" phx-window-keydown={if @show_delete_modal, do: "window_keydown", else: nil} > - <.list> - <:item title={gettext("Email")}>{@user.email} - <:item title={gettext("Role")}>{@user.role.name} - <:item title={gettext("Password Authentication")}> - {if MvWeb.Helpers.UserHelpers.has_password?(@user), - do: gettext("Enabled"), - else: gettext("Not enabled")} - - <:item title={gettext("OIDC")}> - {if MvWeb.Helpers.UserHelpers.has_oidc?(@user), - do: gettext("Linked"), - else: gettext("Not linked")} - - <:item title={gettext("Linked Member")}> - <%= if @user.member do %> - <.link - navigate={~p"/members/#{@user.member}"} - class="text-blue-600 underline hover:text-blue-800" - > - <.icon name="hero-users" class="inline w-4 h-4 mr-1" /> - {MvWeb.Helpers.MemberHelpers.display_name(@user.member)} - - <% else %> - {gettext("No member linked")} - <% end %> - - +
+ <.list> + <:item title={gettext("Email")}>{@user.email} + <:item title={gettext("Role")}>{@user.role.name} + <:item title={gettext("Password Authentication")}> + {if MvWeb.Helpers.UserHelpers.has_password?(@user), + do: gettext("Enabled"), + else: gettext("Not enabled")} + + <:item title={gettext("OIDC")}> + {if MvWeb.Helpers.UserHelpers.has_oidc?(@user), + do: gettext("Linked"), + else: gettext("Not linked")} + + <:item title={gettext("Linked Member")}> + <%= if @user.member do %> + <.link + navigate={~p"/members/#{@user.member}"} + class="text-blue-600 underline hover:text-blue-800" + > + <.icon name="hero-users" class="inline w-4 h-4 mr-1" /> + {MvWeb.Helpers.MemberHelpers.display_name(@user.member)} + + <% else %> + {gettext("No member linked")} + <% end %> + + - <%!-- Danger zone: canonical pattern (same as member show) --%> - <%= if can?(@current_user, :destroy, @user) and not Mv.Helpers.SystemActor.system_user?(@user) do %> -
-

- {gettext("Danger zone")} -

-
-

- {gettext( - "Deleting this user cannot be undone. The user account and any linked member association will be affected." - )} -

- <.button - id="delete-user-trigger" - variant="danger" - phx-click="open_delete_modal" - data-testid="user-delete" - aria-label={gettext("Delete user %{email}", email: @user.email)} - > - <.icon name="hero-trash" class="size-4" /> - {gettext("Delete user")} - -
-
- <% end %> + <%!-- Danger zone: canonical pattern (same as member show) --%> + <%= if can?(@current_user, :destroy, @user) and not Mv.Helpers.SystemActor.system_user?(@user) do %> +
+

+ {gettext("Danger zone")} +

+
+

+ {gettext( + "Deleting this user cannot be undone. The user account and any linked member association will be affected." + )} +

+ <.button + id="delete-user-trigger" + variant="danger" + phx-click="open_delete_modal" + data-testid="user-delete" + aria-label={gettext("Delete user %{email}", email: @user.email)} + > + <.icon name="hero-trash" class="size-4" /> + {gettext("Delete user")} + +
+
+ <% end %> +
<%!-- Delete User Confirmation Modal (WCAG: focus in modal, keyboard confirm/cancel) --%> <%= if assigns[:show_delete_modal] do %>