fix(settings/providers): drop colour palette + filter polish + drawer warnings

Round of design-feedback fixes.

Provider chips
- Drop the per-provider raw Tailwind palette (bg-blue-600 etc.) from
  Provider::Metadata. All cards + drawer logo lock-up now use
  bg-surface-inset + text-primary, matching the design's §04 "drop
  colour entirely" recommendation. Solves the long-standing §01
  BLOCKER without externalising brand assets. Re-introducing logos
  later just means an optional logo_svg: field on metadata.
- ProviderCard component drops the `logo_bg:` parameter; the chip
  is now styled in the template.

Filter / search
- "Available · N" count and the empty-filter state now update
  client-side as the chip filter and free-text search narrow the
  grid (new `count` Stimulus target + dedicated update path).
- Empty-filter state now offers a Clear filters button that resets
  both the search input and the active chip in one click.
- Search placeholder drops the drifting "Search 9 providers" count
  for plain "Search providers" — the section heading carries the
  number.
- Chip labels normalised to plural where natural: "Banks · Crypto ·
  Investments" (Crypto stays as the mass noun).

Drawer copy / treatment
- "IP Whitelisting Required" → "IP whitelisting required" (DS
  sentence-case).
- Binance "do NOT enable withdrawal permissions" lifted out of
  inline red-text into a proper bg-warning-50 border-warning-200
  alert block with an alert-triangle icon. Matches the api_keys /
  hosting alert pattern.
- SnapTrade free-tier inline alert-triangle now uses `size: "sm"`
  so the icon stops rendering at 20px next to 14px body text.

Spacing
- Group-heading margin top bumped 5 → 6 (20→24px) so the eyebrow
  has more breathing room above the search bar.
This commit is contained in:
Guillem Arias
2026-05-09 13:48:09 +02:00
committed by Guillem Arias
parent 8c961958b4
commit 6abceb07ff
11 changed files with 62 additions and 116 deletions

View File

@@ -2,8 +2,8 @@
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-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 class="w-9 h-9 rounded-lg flex items-center justify-center shrink-0 bg-surface-inset">
<span class="text-xs font-bold text-primary"><%= logo_text %></span>
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 flex-wrap">

View File

@@ -10,7 +10,7 @@ class Settings::ProviderCard < ApplicationComponent
end
def initialize(provider_key:, name:, tagline: nil, region: nil, kind: nil, tier: nil,
maturity: :stable, logo_bg: "bg-gray-500", logo_text: nil)
maturity: :stable, logo_text: nil)
@provider_key = provider_key
@name = name
@tagline = tagline
@@ -18,11 +18,10 @@ class Settings::ProviderCard < ApplicationComponent
@kind = kind
@tier = tier
@maturity = maturity.to_sym
@logo_bg = logo_bg
@logo_text = logo_text || name.first(2).upcase
end
attr_reader :name, :tagline, :logo_bg, :logo_text
attr_reader :name, :tagline, :logo_text
def maturity_label
self.class.maturity_label(@maturity)

View File

@@ -2,8 +2,10 @@ import { Controller } from "@hotwired/stimulus";
// Connects to data-controller="providers-filter"
// Filters provider cards by free-text query and a chip-selected kind.
// Updates the visible-count target on the section heading and toggles
// an empty-state target when no card matches.
export default class extends Controller {
static targets = ["input", "chip", "card", "empty"];
static targets = ["input", "chip", "card", "empty", "count"];
static values = { kind: { type: String, default: "all" } };
connect() {
@@ -29,6 +31,10 @@ export default class extends Controller {
if (visible) visibleCount++;
});
if (this.hasCountTarget) {
this.countTarget.textContent = visibleCount;
}
if (this.hasEmptyTarget) {
this.emptyTarget.classList.toggle("hidden", visibleCount > 0);
}
@@ -40,6 +46,14 @@ export default class extends Controller {
this.filter();
}
clear() {
if (this.hasInputTarget) this.inputTarget.value = "";
this.kindValue = "all";
this.syncChipState();
this.filter();
if (this.hasInputTarget) this.inputTarget.focus();
}
syncChipState() {
if (!this.hasChipTarget) return;
this.chipTargets.forEach((chip) => {

View File

@@ -1,97 +1,22 @@
class Provider
module Metadata
REGISTRY = {
simplefin: {
region: "US",
kind: "Bank",
maturity: :stable,
logo_bg: "bg-blue-600",
logo_text: "SF"
},
lunchflow: {
region: "US",
kind: "Bank",
maturity: :stable,
logo_bg: "bg-orange-500",
logo_text: "LF"
},
enable_banking: {
region: "EU",
kind: "Bank",
maturity: :beta,
logo_bg: "bg-purple-600",
logo_text: "EB"
},
coinstats: {
region: "Global",
kind: "Crypto",
maturity: :beta,
logo_bg: "bg-yellow-500",
logo_text: "CS"
},
mercury: {
region: "US",
kind: "Bank",
maturity: :beta,
logo_bg: "bg-cyan-600",
logo_text: "ME"
},
coinbase: {
region: "Global",
kind: "Crypto",
maturity: :beta,
logo_bg: "bg-blue-500",
logo_text: "CB"
},
binance: {
region: "Global",
kind: "Crypto",
maturity: :beta,
logo_bg: "bg-yellow-400",
logo_text: "BI"
},
snaptrade: {
region: "US / CA",
kind: "Investment",
maturity: :beta,
logo_bg: "bg-green-600",
logo_text: "ST"
},
indexa_capital: {
region: "ES",
kind: "Investment",
maturity: :alpha,
logo_bg: "bg-red-600",
logo_text: "IC"
},
sophtron: {
region: "US",
kind: "Bank",
maturity: :alpha,
logo_bg: "bg-teal-600",
logo_text: "SO"
},
plaid: {
region: "US",
kind: "Bank",
tier: "Paid",
maturity: :stable,
logo_bg: "bg-indigo-600",
logo_text: "PL"
},
plaid_eu: {
name: "Plaid EU",
region: "EU",
kind: "Bank",
tier: "Paid",
maturity: :stable,
logo_bg: "bg-indigo-600",
logo_text: "PL"
}
simplefin: { region: "US", kind: "Bank", maturity: :stable, logo_text: "SF" },
lunchflow: { region: "US", kind: "Bank", maturity: :stable, logo_text: "LF" },
enable_banking: { region: "EU", kind: "Bank", maturity: :beta, logo_text: "EB" },
coinstats: { region: "Global", kind: "Crypto", maturity: :beta, logo_text: "CS" },
mercury: { region: "US", kind: "Bank", maturity: :beta, logo_text: "ME" },
coinbase: { region: "Global", kind: "Crypto", maturity: :beta, logo_text: "CB" },
binance: { region: "Global", kind: "Crypto", maturity: :beta, logo_text: "BI" },
snaptrade: { region: "US / CA", kind: "Investment", maturity: :beta, logo_text: "ST" },
indexa_capital: { region: "ES", kind: "Investment", maturity: :alpha, logo_text: "IC" },
sophtron: { region: "US", kind: "Bank", maturity: :alpha, logo_text: "SO" },
plaid: { region: "US", kind: "Bank", tier: "Paid", maturity: :stable, logo_text: "PL" },
plaid_eu: { name: "Plaid EU", region: "EU", kind: "Bank", tier: "Paid", maturity: :stable, logo_text: "PL" }
}.freeze
def self.for(provider_key)
REGISTRY[provider_key.to_sym] || { logo_text: provider_key.to_s.first(2).upcase, logo_bg: "bg-gray-500" }
REGISTRY[provider_key.to_sym] || { logo_text: provider_key.to_s.first(2).upcase }
end
end
end

View File

@@ -8,7 +8,11 @@
<li><%= t("settings.providers.binance_panel.step2") %></li>
<li><%= t("settings.providers.binance_panel.step3") %></li>
</ol>
<p class="text-destructive text-xs font-medium"><%= t("settings.providers.binance_panel.no_withdraw_warning") %></p>
</div>
<div class="bg-warning-50 border border-warning-200 rounded-xl p-3 flex items-start gap-2">
<%= icon "alert-triangle", size: "sm", class: "!w-3.5 !h-3.5 text-warning-600 shrink-0 mt-0.5" %>
<p class="text-sm text-warning-700 font-medium"><%= t("settings.providers.binance_panel.no_withdraw_warning") %></p>
</div>
<div class="bg-surface border border-primary p-3 rounded-lg text-sm">

View File

@@ -3,9 +3,9 @@
<% 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-xs font-bold text-inverse"><%= meta[:logo_text] %></span>
<% if meta && meta[:logo_text].present? %>
<span class="w-7 h-7 rounded-lg flex items-center justify-center shrink-0 bg-surface-inset">
<span class="text-xs font-bold text-primary"><%= meta[:logo_text] %></span>
</span>
<% end %>
<h2 class="text-lg font-medium text-primary truncate"><%= title %></h2>

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-5 mb-1.5 px-1" do %>
<%= tag.div id: anchor.presence, class: "flex items-baseline justify-between gap-3 mt-6 mb-1.5 px-1" do %>
<h2 class="text-xs font-medium text-secondary uppercase flex items-baseline gap-2">
<%= title %>
<% if count %>

View File

@@ -1,4 +1,3 @@
<%# locals: (count:) %>
<div class="flex flex-wrap items-center gap-2.5 mt-5 mb-3">
<div class="relative flex-1 min-w-[200px]">
<input type="search"
@@ -6,7 +5,7 @@
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) %>"
placeholder="<%= t("settings.providers.search_filters.placeholder") %>"
class="block w-full border border-secondary rounded-md py-2.5 pl-10 pr-3 bg-container focus:ring-gray-500 sm:text-sm">
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<%= icon "search", class: "text-secondary" %>

View File

@@ -10,7 +10,7 @@
<li><%= t("providers.snaptrade.step_4") %></li>
</ol>
<p class="text-warning text-sm"><%= icon("alert-triangle", class: "inline-block w-4 h-4 mr-1") %><%= t("providers.snaptrade.free_tier_warning") %></p>
<p class="text-warning text-sm"><%= icon("alert-triangle", size: "sm", class: "inline-block mr-1") %><%= t("providers.snaptrade.free_tier_warning") %></p>
</div>
<% error_msg = local_assigns[:error_message] || @error_message %>

View File

@@ -46,16 +46,23 @@
<% if @available.any? %>
<div data-controller="providers-filter">
<%= render "settings/providers/search_filters", count: @available.size %>
<%= render "settings/providers/search_filters" %>
<%= render "settings/providers/group_heading",
title: t("settings.providers.groups.available"),
count: @available.size,
anchor: "available" %>
<div id="available" class="flex items-baseline justify-between gap-3 mt-6 mb-1.5 px-1">
<h2 class="text-xs font-medium text-secondary uppercase flex items-baseline gap-2">
<%= t("settings.providers.groups.available") %>
<span class="text-subdued font-normal normal-case tabular-nums">
· <span data-providers-filter-target="count"><%= @available.size %></span>
</span>
</h2>
</div>
<p data-providers-filter-target="empty" class="hidden text-sm text-secondary px-1 py-2">
<%= t("settings.providers.empty_filter") %>
</p>
<div data-providers-filter-target="empty" class="hidden text-sm text-secondary px-1 py-2 flex items-center gap-2">
<span><%= t("settings.providers.empty_filter") %></span>
<button type="button" data-action="click->providers-filter#clear" class="text-primary underline">
<%= t("settings.providers.clear_filter") %>
</button>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<% @available.each do |entry| %>
@@ -68,7 +75,6 @@
kind: meta[:kind],
tier: meta[:tier],
maturity: meta[:maturity] || :stable,
logo_bg: meta[:logo_bg],
logo_text: meta[:logo_text]
) %>
<% end %>

View File

@@ -234,15 +234,14 @@ en:
plaid_eu: Connect European financial institutions via Plaid (PSD2 / Open Banking).
search_filters:
aria_label: Search providers
placeholder:
one: "Search %{count} provider"
other: "Search %{count} providers"
placeholder: Search providers
chips:
all: All
bank: Banks
crypto: Crypto
investment: Investment
investment: Investments
empty_filter: No providers match your filter.
clear_filter: Clear filters
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.
@@ -265,7 +264,7 @@ en:
step2: "Create a new API key with Enable Reading permission only"
step3: "Paste your API Key and Secret below"
no_withdraw_warning: "Warning: do NOT enable withdrawal permissions"
ip_hint_title: "IP Whitelisting Required"
ip_hint_title: "IP whitelisting required"
ip_hint_body: "Add the app server's egress IP to the Binance API Key whitelist:"
ip_hint_contact_admin: "Contact your administrator to obtain the app server's egress IP address."
api_key_label: API Key