Files
sure/app/views/plaid_items/_plaid_item.html.erb
Guillem Arias Fauste 834ec19fdc feat(design-system): DS::Disclosure :card_inset variant + migrate ibkr_panel + settings/_section (#1715 §6) (#1857)
* feat(design-system): add :card_inset variant + migrate ibkr_panel and settings/_section

Wraps up the disclosure migration cluster from #1715 §6:

1. **New `:card_inset` variant** on `DS::Disclosure`. Same contract
   as `:card` but uses `bg-surface-inset rounded-xl p-4` (no shadow)
   for inset sub-panels embedded inside a parent card surface.

2. **Migrate `_ibkr_panel.html.erb`** — the "flex query details"
   disclosure (`<details class="group bg-surface-inset rounded-xl p-4">`)
   was the one panel skipped from #1856 because it used the inset
   surface. Now uses `DS::Disclosure(variant: :card_inset)`. Chevron
   gets the `motion-safe:transition-transform motion-safe:duration-150`
   treatment along the way.

3. **Migrate `settings/_section.html.erb`** — the global "collapsible
   settings card" primitive backing 19 callsites via the
   `settings_section(...)` helper. The collapsible branch's
   `<details class="group bg-container shadow-border-xs rounded-xl p-4">`
   becomes `DS::Disclosure(variant: :card, open: open, data: ...)`.

While here:

- Update `disclosure.html.erb` to spread `**opts` onto the `<details>`
  element via `tag.details`. Previously opts were captured but never
  applied; the `settings/_section` migration needs `data-controller`
  + `data-auto-open-param-value` to flow through to the rendered
  `<details>`.
- Non-collapsible branch in `settings/_section.html.erb` stays as
  raw `<section>` — different semantics (not expandable), DS::Disclosure
  can't replace because it always renders `<details>`.

API:

  DS::Disclosure.new(
    variant: :card | :card_inset | :default,
    open: bool,
    data: { controller: "...", ... }   # forwarded to <details>
  )

* fix(review): merge caller class in DS::Disclosure + i18n plaid deletion

- DS::Disclosure: extract caller class: from opts and merge via class_names
  before forwarding to tag.details. Prevents the latent duplicate keyword
  arg error when callers pass class: alongside the variant-derived classes.
- plaid_items/_plaid_item: localize "(deletion in progress...)" via
  t('.deletion_in_progress') + add en locale key, matching lunchflow /
  mercury / sophtron / coinstats convention.

* fix(panels): replace text-white and bg-gray-tint-10 with semantic tokens

`text-white` → `text-inverse` on the EnableBanking reauthorize button
(`bg-warning` background); `bg-gray-tint-10` → `bg-container-inset` on
the IndexaCapital item avatar wrapper. Both flagged by sure-design as
non-functional palette tokens.

Pre-existing on main; surfaced by the re-indentation that this PR
applied during the disclosure migration.
2026-05-21 16:25:01 +02:00

110 lines
4.4 KiB
Plaintext

<%# locals: (plaid_item:) %>
<%= tag.div id: dom_id(plaid_item) do %>
<%= render DS::Disclosure.new(variant: :card, open: true) do |disclosure| %>
<% disclosure.with_summary_content do %>
<div class="flex items-center justify-between gap-2">
<div class="flex items-center gap-2">
<%= icon "chevron-right", class: "group-open:rotate-90 motion-safe:transition-transform motion-safe:duration-150" %>
<div class="flex items-center justify-center h-8 w-8 bg-blue-600/10 rounded-full">
<% if plaid_item.logo.attached? %>
<%= image_tag plaid_item.logo, class: "rounded-full h-full w-full", loading: "lazy" %>
<% else %>
<div class="flex items-center justify-center">
<%= tag.p plaid_item.name.first.upcase, class: "text-blue-600 text-xs font-medium" %>
</div>
<% end %>
</div>
<div class="pl-1 text-sm">
<div class="flex items-center gap-2">
<%= tag.p plaid_item.name, class: "font-medium text-primary" %>
<% if plaid_item.scheduled_for_deletion? %>
<p class="text-destructive text-sm animate-pulse"><%= t(".deletion_in_progress") %></p>
<% end %>
</div>
<% if plaid_item.syncing? %>
<div class="text-secondary flex items-center gap-1">
<%= icon "loader", size: "sm", class: "animate-pulse" %>
<%= tag.span t(".syncing") %>
</div>
<% elsif plaid_item.requires_update? %>
<div class="text-warning flex items-center gap-1">
<%= icon "alert-triangle", size: "sm", color: "warning" %>
<%= tag.span t(".requires_update") %>
</div>
<% elsif plaid_item.sync_error.present? %>
<div class="text-secondary flex items-center gap-1">
<%= icon "alert-circle", size: "sm", color: "destructive" %>
<%= tag.span t(".error"), class: "text-destructive" %>
</div>
<% else %>
<p class="text-secondary">
<%= plaid_item.last_synced_at ? t(".status", timestamp: time_ago_in_words(plaid_item.last_synced_at)) : t(".status_never") %>
</p>
<% end %>
</div>
</div>
<% if Current.user&.admin? %>
<div class="flex items-center gap-2">
<% if plaid_item.requires_update? %>
<%= render DS::Link.new(
text: t(".update"),
icon: "refresh-cw",
variant: "secondary",
href: edit_plaid_item_path(plaid_item),
frame: "modal"
) %>
<% elsif Rails.env.development? %>
<%= icon(
"refresh-cw",
as_button: true,
href: sync_plaid_item_path(plaid_item)
) %>
<% end %>
<%= render DS::Menu.new do |menu| %>
<% menu.with_item(
variant: "button",
text: t(".delete"),
icon: "trash-2",
href: plaid_item_path(plaid_item),
method: :delete,
confirm: CustomConfirm.for_resource_deletion(plaid_item.name, high_severity: true)
) %>
<% end %>
</div>
<% end %>
</div>
<% end %>
<% unless plaid_item.scheduled_for_deletion? %>
<div class="space-y-4 mt-4">
<% if plaid_item.accounts.any? %>
<%= render "accounts/index/account_groups", accounts: plaid_item.accounts %>
<% end %>
<%# Sync summary (collapsible) - using shared ProviderSyncSummary component %>
<% stats = if defined?(@plaid_sync_stats_map) && @plaid_sync_stats_map
@plaid_sync_stats_map[plaid_item.id] || {}
else
plaid_item.syncs.ordered.first&.sync_stats || {}
end %>
<%= render ProviderSyncSummary.new(
stats: stats,
provider_item: plaid_item
) %>
<% if plaid_item.accounts.empty? %>
<div class="p-4 flex flex-col gap-3 items-center justify-center">
<p class="text-primary font-medium text-sm"><%= t(".no_accounts_title") %></p>
<p class="text-secondary text-sm"><%= t(".no_accounts_description") %></p>
</div>
<% end %>
</div>
<% end %>
<% end %>
<% end %>