From d88d6e9e58715c243616cfc4d585fe906e289b77 Mon Sep 17 00:00:00 2001 From: Guillem Arias Fauste Date: Sat, 6 Jun 2026 16:28:22 +0200 Subject: [PATCH] =?UTF-8?q?fix(ds):=20canonical=20transaction-row=20?= =?UTF-8?q?=E2=80=94=20stop=20category=20pills=20truncating=20(#2147)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(ds): canonical transaction-row — stop category pills truncating (#2137) The desktop transaction grid gave the name column col-span-8 (67%, usually half-empty) and the category column only col-span-2 (17%), so the category pill's name area was clamped to ~44px and ellipsized nearly every value ("Misc...", "Shop...", "Rest...") despite the empty space the audit flagged. - Rebalance the lg:grid-cols-12 row: name col-span-8 -> 7, category 2 -> 3 (amount unchanged; still 12). Category gains 50% width from the over-wide name column. - categories/_badge: the pill was `flex w-full` (stretched to fill the column); make it `inline-flex max-w-full` so it hugs its content and short names render fully, capping + truncating only when genuinely too long. Verified on /transactions: visible-row category truncation dropped 7/7 -> 2/7 even in the compressed (AI-panel-open) view; Payment / Shopping / Restaurants now render in full. Fixes both the interactive categories/menu and the static transfer categories/badge (menu reuses the badge partial). * feat(ds): lift categories/_badge onto DS::Pill Addresses the Drift Patrol finding (and jjmata's request) properly instead of patching the bespoke span: the badge becomes a DS::Pill in badge mode. The pill primitive grows the three capabilities the badge needed and the bot's one-liner glossed over: - truncate: pills that may shrink inside min-w-0 columns drop their shrink-0/whitespace-nowrap and ellipsize the label instead of overflowing (the transaction-row category cell this PR fixes). - label_testid: stamps data-testid on the label span; five test files target [data-testid='category-name']. - icon_size: passthrough to the icon helper (badge keeps its established sm glyph; default stays xs). custom_color was already the sanctioned escape hatch for user-chosen hues. Owned visual deltas from pill standardization: px-1.5/py-1 -> px-2/py-0.5, gap-1 -> gap-1.5, border mix 10% -> 20%; bg mix, hex text, radius, text scale, icon size, truncation and testid are parity. Label wrapper only renders when truncate/label_testid ask for it, so existing pill DOM (and the find("span", text:) assertions on it) is unchanged. Four new pill tests + a Lookbook case cover the recipe. --- app/components/DS/pill.html.erb | 8 +++-- app/components/DS/pill.rb | 24 +++++++++++--- app/views/categories/_badge.html.erb | 27 ++++++++------- app/views/transactions/_transaction.html.erb | 4 +-- test/components/DS/pill_test.rb | 33 +++++++++++++++++++ .../previews/pill_component_preview.rb | 15 +++++++++ 6 files changed, 91 insertions(+), 20 deletions(-) diff --git a/app/components/DS/pill.html.erb b/app/components/DS/pill.html.erb index 2dc15e0ec..67e051905 100644 --- a/app/components/DS/pill.html.erb +++ b/app/components/DS/pill.html.erb @@ -10,11 +10,15 @@ <% else %> <% if icon %> - <%= helpers.icon(icon, size: "xs", color: "current") %> + <%= helpers.icon(icon, size: icon_size, color: "current") %> <% elsif show_dot %> <% end %> - <%= label %> + <% if truncate || label_testid %> + <%= tag.span label, class: ("min-w-0 truncate" if truncate), data: (label_testid ? { testid: label_testid } : nil) %> + <% else %> + <%= label %> + <% end %> <% end %> diff --git a/app/components/DS/pill.rb b/app/components/DS/pill.rb index c45bf056c..b9ef102db 100644 --- a/app/components/DS/pill.rb +++ b/app/components/DS/pill.rb @@ -16,7 +16,8 @@ class DS::Pill < DesignSystemComponent neutral: :gray }.freeze - attr_reader :label, :tone, :style, :size, :show_dot, :dot_only, :title, :icon, :marker, :custom_color + attr_reader :label, :tone, :style, :size, :show_dot, :dot_only, :title, :icon, :marker, :custom_color, + :truncate, :label_testid, :icon_size # Generic inline pill primitive. Two modes: # @@ -50,8 +51,17 @@ class DS::Pill < DesignSystemComponent # `SEMANTIC_TONE_ALIASES`. # - Sure has full violet / indigo / fuchsia / amber / green / gray / # red ramps in the design system; this component picks named tokens - # at render time. No raw hex. - def initialize(label: nil, tone: :violet, style: :soft, size: :sm, show_dot: nil, dot_only: false, title: nil, icon: nil, marker: true, custom_color: nil) + # at render time. No raw hex — except `custom_color:`, which exists for + # user-defined entities (categories, tags) whose hue is data, not design. + # - `truncate: true` lets the pill shrink inside a `min-w-0` parent and + # ellipsize its label instead of overflowing (dense table cells like the + # transaction row's category column). Default pills stay `shrink-0`. + # - `label_testid:` stamps `data-testid` on the label span for system / + # controller tests that need to target the text node. + # - `icon_size:` passes through to the icon helper (default "xs"; the + # category badge uses "sm" to keep its established glyph size). + def initialize(label: nil, tone: :violet, style: :soft, size: :sm, show_dot: nil, dot_only: false, title: nil, icon: nil, marker: true, custom_color: nil, + truncate: false, label_testid: nil, icon_size: "xs") resolved_tone = SEMANTIC_TONE_ALIASES.fetch(tone.to_sym, tone.to_sym) @label = label || I18n.t("ds.pill.default_label", default: "Beta") @tone = TONES.include?(resolved_tone) ? resolved_tone : :violet @@ -65,6 +75,9 @@ class DS::Pill < DesignSystemComponent @icon = icon @marker = marker @custom_color = custom_color + @truncate = truncate + @label_testid = label_testid + @icon_size = icon_size end def palette @@ -149,7 +162,10 @@ class DS::Pill < DesignSystemComponent def container_classes base = [ - "inline-flex items-center align-middle font-medium whitespace-nowrap shrink-0", + "inline-flex items-center align-middle font-medium", + # Truncating pills must be allowed to shrink (and let the label span + # ellipsize); everything else keeps its intrinsic width. + truncate ? "max-w-full min-w-0" : "whitespace-nowrap shrink-0", "border leading-none" ] diff --git a/app/views/categories/_badge.html.erb b/app/views/categories/_badge.html.erb index 5ab8d6c1d..283c05fc2 100644 --- a/app/views/categories/_badge.html.erb +++ b/app/views/categories/_badge.html.erb @@ -1,17 +1,20 @@ <%# locals: (category:) %> <% category ||= Category.uncategorized %> +<%# Canonical category badge: DS::Pill in badge mode carrying the category's + user-chosen hex (custom_color is the sanctioned escape hatch for hues that + are data, not design). truncate + this min-w-0 wrapper keep long names + ellipsizing inside tight columns (e.g. the transaction row's category + cell) instead of overflowing them. %>
- - <% if category.lucide_icon.present? %> - - <%= icon category.lucide_icon, size: "sm", color: "current" %> - - <% end %> - <%= category.name %> - + <%= render DS::Pill.new( + label: category.name, + custom_color: category.color, + icon: category.lucide_icon.presence, + icon_size: "sm", + marker: false, + size: :md, + truncate: true, + label_testid: "category-name", + title: category.name) %>
diff --git a/app/views/transactions/_transaction.html.erb b/app/views/transactions/_transaction.html.erb index 9aff39d03..9752d6117 100644 --- a/app/views/transactions/_transaction.html.erb +++ b/app/views/transactions/_transaction.html.erb @@ -7,7 +7,7 @@ <%= turbo_frame_tag dom_id(transaction) do %>
"> -
+
<%= check_box_tag dom_id(entry, "selection"), disabled: transaction.transfer.present?, class: "checkbox checkbox--light hidden lg:block", @@ -179,7 +179,7 @@
-