refactor(settings/providers): tighten paddings, dedupe maturity badge, semantic + a11y fixes

Pixel-level alignment to the design's §05 mock + cleanup from a DS
audit pass.

Paddings, margins, font sizes
- Health strip: my-4 → mt-4 mb-5 to match the design's 16px / 20px
  vertical breathing room.
- Search filters bar: gap-2 → gap-2.5; mt-2 → mt-5 mb-3 (was missing
  the 12px bottom margin entirely).
- Search box: rounded-lg → rounded-[10px]; px-3 py-2 → px-[14px]
  py-[9px]. Search icon downsized w-4 → w-3.5 to match.
- Chip group: p-1 → p-[3px]; rounded-lg → rounded-[10px].
- Chip: py-1 → py-[5px]; rounded-md → rounded-lg.
- Group heading: mt-2 → mt-[18px]; mb-1 → mb-1.5.
- Status pill: text-xs → text-[11px].
- Provider card: gap-3 → gap-2.5 (outer + top); name gets explicit
  text-sm; tagline + foot 14px → 13px; arrow icon w-4 → w-3.5.
- Sync icon button: p-1 → fixed w-7 h-7 (28×28) so the row hit
  target matches the design's column width.
- Connect drawer header logo glyph: text-[10px] → text-xs (matches
  the available card's logo-glyph treatment).

Component / partial cleanup (DS audit follow-ups)
- New _maturity_badge partial replaces the inline span that was
  duplicated in 3 places (_connection_row, _drawer_header,
  provider_card.html.erb).
- Settings::ProviderCard.maturity_label class method centralizes the
  MATURITY_LABELS lookup; callers no longer reach into the constant.
- _connection_row title: <h2> → <h3> (the row sits inside the
  "Your connections" h2 group heading; nested h2s flattened the
  outline).
- show.html.erb encryption error: <h3> → <h2> for the same reason.

Locale
- Drop orphaned keys: settings.providers.groups.connected and
  groups.needs_attention (no view code uses them) plus the leftover
  show.coinbase_title block.
- Health strip "needs reconsent" → "needs attention" so the strip
  copy lines up with the per-row status pill ("Action needed") and
  the original group heading wording.

A11y
- focus-visible:ring-2 on chip buttons, provider-card link, and
  focus-within:ring-2 on the search input wrapper. Keyboard users
  now get a visible focus state.
- Search input: explicit autocomplete="off" (erb_lint hint).
This commit is contained in:
Guillem Arias
2026-05-09 12:29:34 +02:00
committed by Guillem Arias
parent b019944824
commit e0aab867d6
13 changed files with 35 additions and 37 deletions

View File

@@ -1,16 +1,14 @@
<%= link_to connect_path,
class: "bg-container shadow-border-xs rounded-xl p-4 flex flex-col gap-3 text-primary hover:bg-container-hover transition-colors",
class: "bg-container shadow-border-xs rounded-xl p-4 flex flex-col gap-2.5 text-primary hover:bg-container-hover transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-alpha-black-100",
data: { turbo_frame: "drawer", turbo_prefetch: "false" }.merge(filter_data) do %>
<div class="flex items-start gap-3">
<div class="flex items-start gap-2.5">
<div class="w-9 h-9 rounded-lg flex items-center justify-center shrink-0 <%= logo_bg %>">
<span class="text-xs font-bold text-inverse"><%= logo_text %></span>
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 flex-wrap">
<span class="font-medium text-primary"><%= name %></span>
<% if maturity_label %>
<span class="text-xs font-medium px-1.5 py-0.5 rounded-full bg-alpha-black-50 text-secondary"><%= maturity_label %></span>
<% end %>
<span class="text-sm font-medium text-primary"><%= name %></span>
<%= render "settings/providers/maturity_badge", label: maturity_label %>
</div>
<% if meta_line.present? %>
<p class="text-xs text-subdued mt-0.5"><%= meta_line %></p>
@@ -18,10 +16,10 @@
</div>
</div>
<% if tagline.present? %>
<p class="text-sm text-secondary grow"><%= tagline %></p>
<p class="text-[13px] text-secondary grow leading-snug"><%= tagline %></p>
<% end %>
<div class="flex items-center justify-end gap-1.5 text-sm font-medium text-primary">
<div class="flex items-center justify-end gap-1.5 text-[13px] font-medium text-primary">
<%= t("settings.providers.connect") %>
<%= helpers.icon "arrow-right", class: "w-4 h-4" %>
<%= helpers.icon "arrow-right", class: "w-3.5 h-3.5" %>
</div>
<% end %>

View File

@@ -4,6 +4,11 @@ class Settings::ProviderCard < ApplicationComponent
alpha: "settings.providers.maturity.alpha"
}.freeze
def self.maturity_label(maturity)
key = MATURITY_LABELS[maturity&.to_sym]
I18n.t(key) if key
end
def initialize(provider_key:, name:, tagline: nil, region: nil, kind: nil, tier: nil,
maturity: :stable, logo_bg: "bg-gray-500", logo_text: nil)
@provider_key = provider_key

View File

@@ -11,8 +11,7 @@
end
sync_action = entry[:partial].present? ? render("settings/providers/sync_button", provider_key: entry[:provider_key], last_synced_at: last_synced) : nil
status_pill = render("settings/providers/status_pill", status: status)
maturity_key = Settings::ProviderCard::MATURITY_LABELS[entry[:maturity]]
maturity_lbl = maturity_key ? t(maturity_key) : nil
maturity_lbl = Settings::ProviderCard.maturity_label(entry[:maturity])
%>
<details <%= "open" if open %>
class="group bg-container shadow-border-xs rounded-xl <%= border_class %>"
@@ -20,10 +19,8 @@
<summary class="flex items-center gap-3 px-4 py-3.5 cursor-pointer rounded-xl list-none [&::-webkit-details-marker]:hidden">
<%= icon "chevron-right", class: "w-3.5 h-3.5 text-secondary group-open:rotate-90 transition-transform shrink-0" %>
<div class="flex items-center gap-2 flex-wrap min-w-0 flex-1">
<h2 class="text-sm font-medium text-primary"><%= entry[:title] %></h2>
<% if maturity_lbl %>
<span class="text-xs font-medium px-1.5 py-0.5 rounded-full bg-alpha-black-50 text-secondary"><%= maturity_lbl %></span>
<% end %>
<h3 class="text-sm font-medium text-primary"><%= entry[:title] %></h3>
<%= render "settings/providers/maturity_badge", label: maturity_lbl %>
</div>
<div class="flex items-center gap-2 shrink-0 group-open:hidden">
<% if meta.present? %>

View File

@@ -1,18 +1,15 @@
<%# locals: (provider_key:, title:) %>
<% meta = provider_key.present? ? Provider::Metadata.for(provider_key) : nil %>
<% maturity_label_key = meta && Settings::ProviderCard::MATURITY_LABELS[meta[:maturity]] %>
<% maturity_label = maturity_label_key ? t(maturity_label_key) : nil %>
<% maturity_label = meta ? Settings::ProviderCard.maturity_label(meta[:maturity]) : nil %>
<div class="flex items-center justify-between gap-2">
<div class="flex items-center gap-2 min-w-0">
<% if meta && meta[:logo_bg].present? %>
<span class="w-7 h-7 rounded-lg flex items-center justify-center shrink-0 <%= meta[:logo_bg] %>">
<span class="text-[10px] font-bold text-inverse"><%= meta[:logo_text] %></span>
<span class="text-xs font-bold text-inverse"><%= meta[:logo_text] %></span>
</span>
<% end %>
<h2 class="text-lg font-medium text-primary truncate"><%= title %></h2>
<% if maturity_label %>
<span class="text-xs font-medium px-1.5 py-0.5 rounded-full bg-alpha-black-50 text-secondary shrink-0"><%= maturity_label %></span>
<% end %>
<%= render "settings/providers/maturity_badge", label: maturity_label %>
</div>
<%= render DS::Button.new(
variant: "icon",

View File

@@ -1,5 +1,5 @@
<%# locals: (title:, count: nil, description: nil, anchor: nil) %>
<%= tag.div id: anchor.presence, class: "flex items-baseline justify-between gap-3 mt-2 mb-1 px-1" do %>
<%= tag.div id: anchor.presence, class: "flex items-baseline justify-between gap-3 mt-[18px] mb-1.5 px-1" do %>
<h2 class="text-sm font-medium text-primary flex items-baseline gap-2">
<%= title %>
<% if count %>

View File

@@ -1,5 +1,5 @@
<%# locals: (connected:, needs_attention:, accounts_syncing:, last_synced_at:) %>
<div class="bg-container shadow-border-xs rounded-xl flex items-center gap-[18px] px-[14px] py-2.5 text-[13px] my-4">
<div class="bg-container shadow-border-xs rounded-xl flex items-center gap-[18px] px-[14px] py-2.5 text-[13px] mt-4 mb-5">
<span class="inline-flex items-center gap-2">
<%= icon "check", class: "w-3.5 h-3.5 text-success" %>
<span class="font-medium tabular-nums"><%= connected %></span>

View File

@@ -0,0 +1,4 @@
<%# locals: (label:) %>
<% if label %>
<span class="text-[10px] font-medium px-1.5 py-px rounded-full bg-alpha-black-50 text-secondary"><%= label %></span>
<% end %>

View File

@@ -1,15 +1,16 @@
<%# locals: (count:) %>
<div class="flex flex-wrap items-center gap-2 mt-2">
<div class="flex-1 min-w-[200px] flex items-center gap-2 bg-container shadow-border-xs rounded-lg px-3 py-2">
<%= icon "search", class: "w-4 h-4 text-secondary shrink-0" %>
<div class="flex flex-wrap items-center gap-2.5 mt-5 mb-3">
<div class="flex-1 min-w-[200px] flex items-center gap-2 bg-container shadow-border-xs rounded-[10px] px-[14px] py-[9px] focus-within:ring-2 focus-within:ring-alpha-black-100">
<%= icon "search", class: "w-3.5 h-3.5 text-secondary shrink-0" %>
<input type="search"
autocomplete="off"
data-providers-filter-target="input"
data-action="input->providers-filter#filter"
aria-label="<%= t("settings.providers.search_filters.aria_label") %>"
placeholder="<%= t("settings.providers.search_filters.placeholder", count: count) %>"
class="bg-transparent border-0 outline-none flex-1 text-sm text-primary placeholder:text-subdued">
</div>
<div class="inline-flex items-center gap-1 p-1 bg-surface-inset rounded-lg">
<div class="inline-flex items-center gap-1 p-[3px] bg-surface-inset rounded-[10px]">
<% %w[all bank crypto investment].each do |kind| %>
<% active = kind == "all" %>
<button type="button"
@@ -17,7 +18,7 @@
data-action="click->providers-filter#selectChip"
data-kind="<%= kind %>"
aria-pressed="<%= active %>"
class="px-2.5 py-1 text-xs font-medium rounded-md transition-colors <%= active ? "bg-container shadow-border-xs text-primary" : "text-secondary" %>">
class="px-2.5 py-[5px] text-xs font-medium rounded-lg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-alpha-black-100 <%= active ? "bg-container shadow-border-xs text-primary" : "text-secondary" %>">
<%= t("settings.providers.search_filters.chips.#{kind}") %>
</button>
<% end %>

View File

@@ -1,7 +1,7 @@
<%# locals: (status:) %>
<% classes = status_pill_classes(status) %>
<% dot_class, pill_class = classes[:dot], classes[:pill] %>
<span class="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs font-medium <%= pill_class %>">
<span class="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-[11px] font-medium <%= pill_class %>">
<span class="w-1.5 h-1.5 rounded-full flex-shrink-0 <%= dot_class %>"></span>
<%= t("settings.providers.status.#{status}") %>
</span>

View File

@@ -6,7 +6,7 @@
disabled: recently_synced,
title: button_label,
aria: { label: button_label },
class: "inline-flex items-center p-1 rounded text-secondary hover:text-primary hover:bg-alpha-black-50 transition-colors disabled:opacity-40 disabled:cursor-not-allowed",
class: "inline-flex items-center justify-center w-7 h-7 rounded-md text-secondary hover:text-primary hover:bg-alpha-black-50 transition-colors disabled:opacity-40 disabled:cursor-not-allowed",
form: { onclick: "event.stopPropagation()", class: "inline-flex" } do %>
<%= icon "refresh-cw", class: "w-3.5 h-3.5" %>
<% end %>

View File

@@ -6,7 +6,7 @@
<div class="flex items-start gap-3">
<%= icon("triangle-alert", class: "w-5 h-5 text-destructive shrink-0 mt-0.5") %>
<div>
<h3 class="font-medium text-primary"><%= t("settings.providers.encryption_error.title") %></h3>
<h2 class="font-medium text-primary"><%= t("settings.providers.encryption_error.title") %></h2>
<p class="text-secondary text-sm mt-1"><%= t("settings.providers.encryption_error.message") %></p>
</div>
</div>

View File

@@ -202,13 +202,11 @@ en:
connect: Connect
groups:
your_connections: Your connections
connected: Connected
needs_attention: Action needed
available: Available
empty_available: All available providers are connected.
health_strip:
connected: connected
needs_attention: needs reconsent
needs_attention: needs attention
accounts_syncing: accounts syncing
last_synced: Last synced %{time} ago
meta:
@@ -249,8 +247,6 @@ en:
crypto: Crypto
investment: Investment
empty_filter: No providers match your filter.
show:
coinbase_title: Coinbase
encryption_error:
title: Encryption Configuration Required
message: Active Record encryption keys are not configured. Please ensure the encryption credentials (active_record_encryption.primary_key, active_record_encryption.deterministic_key, and active_record_encryption.key_derivation_salt) are properly set up in your Rails credentials or environment variables before using sync providers.

View File

@@ -33,7 +33,7 @@ class Settings::ProvidersTest < ApplicationSystemTestCase
visit settings_providers_path
titles = all("details").map { |d| d.find("summary h2", match: :first).text.squish }
titles = all("details").map { |d| d.find("summary h3", match: :first).text.squish }
assert_equal titles.sort_by(&:downcase), titles, "Connection panels should render alphabetically by title"
connections_heading = page.find(:xpath, "//h2[contains(normalize-space(), 'Your connections')]")