Files
sure/app/views/settings/profiles/show.html.erb
Guillem Arias Fauste 8736c7c27e refactor(settings): consistency pass — header-less settings_section + guides (#2279)
* refactor(settings): header-less settings_section variant + migrate guides

Add a title:nil branch to settings_section so a page can route through the one
canonical surface recipe (bg-container shadow-border-xs rounded-xl p-4 space-y-4)
without a section header. No visual change to existing titled callers.

Migrate the guides page onto it: replace the hand-rolled card + hardcoded
'Guides' page title with settings_section + t('.page_title') (new locale key).
First step of the settings design-consistency pass.

* refactor(family-exports): un-nest exports list

The exports list rendered 'Exports' three times (page h1 + settings_section
title + inset count header) inside three stacked surfaces (section card >
bg-container-inset > inner bg-container table card). Flatten it:
- settings_section header-less (drop the duplicate title; the page h1 + the
  inset count header already label it),
- drop the redundant inner space-y-4 wrapper,
- table sits directly in the inset (remove the inner bg-container card).

Now: one title, one card, one inset.

* refactor(settings): payments + appearances consistency

- payments: subscription summary row was bg-container inside the section's
  bg-container (container-on-container) -> bg-container-inset + p-4.
- appearances: toggle-row labels used <h4> (heading-level break inside a
  settings_section) -> <p font-medium>.

* refactor(settings): preferences consistency

- month_start_day warning: text-warning bg-warning/10 colored-body-text box
  -> DS::Alert(:warning) (neutral-text recipe).
- preview-features block: hand-rolled <section bg-container shadow-border-xs
  rounded-xl p-4> -> header-less settings_section (canonical surface).
- toggle-row <h4> -> <p font-medium>; text-[11px] base-currency badge -> text-xs.

* refactor(settings): profiles consistency

- unconfirmed-email notice was hardcoded English -> i18n
  (unconfirmed_email_notice_html + resend_confirmation_link keys).
- role + pending chips: bespoke 'rounded-md bg-surface px-1.5 py-0.5 uppercase'
  pills -> DS::Pill (tone: gray, badge mode).
- pending-invitation row border: border-alpha-black-25 -> shadow-border-xs
  (match the member-row token).
2026-06-11 15:48:53 +02:00

188 lines
8.7 KiB
Plaintext

<%= content_for :page_title, t(".page_title") %>
<%= settings_section title: t(".profile_title"), subtitle: t(".profile_subtitle", product_name: product_name) do %>
<%= styled_form_with model: @user, url: user_path(@user), class: "space-y-4" do |form| %>
<%= render "settings/user_avatar_field", form: form, user: @user %>
<div>
<%= form.email_field :email, placeholder: t(".email"), label: t(".email") %>
<% if @user.unconfirmed_email.present? %>
<p class="mt-2 text-sm text-secondary">
<%= t(".unconfirmed_email_notice_html",
email: @user.unconfirmed_email,
resend_link: link_to(t(".resend_confirmation_link"), resend_confirmation_email_user_path(@user), class: "hover:underline text-secondary")) %>
</p>
<% end %>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
<%= form.text_field :first_name, placeholder: t(".first_name"), label: t(".first_name") %>
<%= form.text_field :last_name, placeholder: t(".last_name"), label: t(".last_name") %>
</div>
<div class="flex justify-end mt-4">
<%= render DS::Button.new(text: t(".save"), type: :submit, class: "md:w-auto w-full justify-center") %>
</div>
</div>
<% end %>
<% end %>
<% unless Current.user.ui_layout_intro? %>
<%= settings_section title: Current.family&.moniker == "Group" ? t(".group_title", default: "Group") : t(".household_title"), subtitle: t(".household_subtitle", moniker_plural: family_moniker_plural_downcase, moniker: family_moniker_downcase) do %>
<div class="space-y-4">
<%= styled_form_with model: Current.user, class: "space-y-4", data: { controller: "auto-submit-form" } do |form| %>
<%= form.fields_for :family do |family_fields| %>
<% name_label = Current.family&.moniker == "Group" ? t(".group_form_label", default: "Group name") : t(".household_form_label") %>
<% name_placeholder = Current.family&.moniker == "Group" ? t(".group_form_input_placeholder", default: "Enter group name") : t(".household_form_input_placeholder") %>
<%= family_fields.text_field :name,
placeholder: name_placeholder,
label: name_label,
disabled: !Current.user.admin?,
"data-auto-submit-form-target": "auto" %>
<% end %>
<% end %>
<div class="bg-container-inset rounded-xl p-1">
<div class="px-4 py-2">
<p class="uppercase text-xs text-secondary font-medium"><%= Current.family.name %> &middot; <%= Current.family.users.size %></p>
</div>
<% @users.each do |user| %>
<div class="flex gap-2 mt-2 items-center bg-container p-4 shadow-border-xs rounded-lg">
<div class="w-9 h-9 shrink-0">
<%= render "settings/user_avatar", avatar_url: user.profile_image&.variant(:small)&.url, initials: user.initials %>
</div>
<p class="text-primary font-medium text-sm"><%= user.display_name %></p>
<%= render DS::Pill.new(label: t("users.roles.#{user.role}", default: user.role.humanize), tone: :gray, marker: false, size: :sm) %>
<% if Current.user.admin? && user != Current.user %>
<div class="ml-auto">
<%= render DS::Button.new(
variant: "icon",
icon: "x",
href: settings_profile_path(user_id: user),
method: :delete,
confirm: CustomConfirm.for_resource_deletion(user.display_name, high_severity: true)
) %>
</div>
<% end %>
</div>
<% end %>
<% if @pending_invitations.any? %>
<% @pending_invitations.each do |invitation| %>
<div class="flex gap-2 items-center justify-between bg-container p-4 shadow-border-xs rounded-lg">
<div class="flex gap-2 items-center">
<div class="w-9 h-9 shrink-0">
<div class="text-inverse w-full h-full bg-surface-inset rounded-full flex items-center justify-center text-lg uppercase"><%= invitation.email[0] %></div>
</div>
<div class="flex items-center gap-2">
<p class="text-primary font-medium text-sm"><%= invitation.email %></p>
<%= render DS::Pill.new(label: t(".pending"), tone: :gray, marker: false, size: :sm) %>
</div>
</div>
<div class="flex items-center gap-4">
<% if self_hosted? %>
<div class="flex items-center gap-2" data-controller="clipboard">
<p class="text-secondary text-sm"><%= t(".invitation_link") %></p>
<span data-clipboard-target="source" class="hidden"><%= accept_invitation_url(invitation.token) %></span>
<input type="text"
readonly
autocomplete="off"
value="<%= accept_invitation_url(invitation.token) %>"
class="text-sm bg-surface-inset px-2 py-1 rounded border border-secondary w-72">
<button data-action="clipboard#copy" class="text-secondary hover:text-primary">
<span data-clipboard-target="iconDefault">
<%= icon "copy" %>
</span>
<span class="hidden" data-clipboard-target="iconSuccess">
<%= icon "check" %>
</span>
</button>
</div>
<% end %>
<% if Current.user.admin? %>
<%= render DS::Button.new(
variant: "icon",
icon: "x",
href: invitation_path(invitation),
method: :delete,
confirm: CustomConfirm.for_resource_deletion(invitation.email, high_severity: true)
) %>
<% end %>
</div>
</div>
<% end %>
<% end %>
<% if Current.user.admin? %>
<%= link_to new_invitation_path,
class: "bg-container-inset flex items-center justify-center gap-2 text-secondary mt-1 hover:bg-container-inset-hover rounded-lg px-4 py-2 w-full text-center",
data: { turbo_frame: :modal } do %>
<%= icon("plus") %>
<%= t(".invite_member") %>
<% end %>
<% end %>
</div>
</div>
<% end %>
<% end %>
<%= settings_section title: t(".danger_zone_title") do %>
<div class="space-y-4">
<% if Current.user.admin? %>
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div class="w-full md:w-2/3">
<h3 class="font-medium text-primary"><%= t(".reset_account") %></h3>
<p class="text-secondary text-sm"><%= t(".reset_account_warning") %></p>
</div>
<%= render DS::Button.new(
text: t(".reset_account"),
variant: "destructive",
href: reset_user_path(@user),
method: :delete,
confirm: CustomConfirm.new(
title: t(".confirm_reset.title"),
body: t(".confirm_reset.body"),
btn_text: t(".reset_account"),
destructive: true,
high_severity: true
)
) %>
</div>
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div class="w-full md:w-2/3">
<h3 class="font-medium text-primary"><%= t(".reset_account_with_sample_data") %></h3>
<p class="text-secondary text-sm"><%= t(".reset_account_with_sample_data_warning") %></p>
</div>
<%= render DS::Button.new(
text: t(".reset_account_with_sample_data"),
variant: "destructive",
href: reset_with_sample_data_user_path(@user),
method: :delete,
confirm: CustomConfirm.new(
title: t(".confirm_reset_with_sample_data.title"),
body: t(".confirm_reset_with_sample_data.body"),
btn_text: t(".reset_account_with_sample_data"),
destructive: true,
high_severity: true
)
) %>
</div>
<% end %>
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div class="w-full md:w-2/3">
<h3 class="font-medium text-primary"><%= t(".delete_account") %></h3>
<p class="text-secondary text-sm"><%= t(".delete_account_warning") %></p>
</div>
<%= render DS::Button.new(
text: t(".delete_account"),
variant: "destructive",
href: user_path(@user),
method: :delete,
confirm: CustomConfirm.for_resource_deletion("your account", high_severity: true)
) %>
</div>
</div>
<% end %>