diff --git a/app/components/DS/disclosure.html.erb b/app/components/DS/disclosure.html.erb index 6923183b5..876dc054c 100644 --- a/app/components/DS/disclosure.html.erb +++ b/app/components/DS/disclosure.html.erb @@ -1,4 +1,4 @@ -
> +<%= tag.details class: details_classes, open: open, **details_opts do %> <%= tag.summary class: summary_classes do %> <% if summary_content? %> <%# `` is `display: list-item`, so a flex inner div would @@ -28,4 +28,4 @@
<%= content %>
-
+<% end %> diff --git a/app/components/DS/disclosure.rb b/app/components/DS/disclosure.rb index a634b2143..5f993d6c1 100644 --- a/app/components/DS/disclosure.rb +++ b/app/components/DS/disclosure.rb @@ -1,46 +1,77 @@ class DS::Disclosure < DesignSystemComponent renders_one :summary_content - VARIANTS = %i[default card].freeze + VARIANTS = %i[default card card_inset inline].freeze attr_reader :title, :align, :open, :variant, :opts # `:default` — bg-surface summary, no chrome on the `
`. Use - # for inline expanders inside a parent card. + # for inline expanders that sit inside a parent card (the summary + # itself reads as the surface). # # `:card` — `
` itself becomes a `bg-container shadow-border-xs # rounded-xl` card; the summary inherits the container (no own bg). # Use for provider-item rows (binance, lunchflow, plaid, etc.) where # each card is the surface and the summary is custom rich content. - # Callers in `:card` mode should pass their own `summary_content` - # slot; the built-in title rendering assumes the `:default` shape. + # + # `:card_inset` — `
` is `bg-surface-inset rounded-xl` (no + # shadow). Use for inset sub-panels inside a parent card surface + # (e.g. the IBKR flex-query "report details" panel embedded inside + # the IBKR settings flow). Same summary contract as `:card`. + # + # `:inline` — no surface, no padding, no shadow. The disclosure reads + # as a plain text-link-style toggle (e.g. "Alternative auth" inside + # a form, or a "Manage connections" lazy-load opener). Caller provides + # the summary text (and optional chevron) via the `summary_content` + # slot. + # + # In card / inline variants, callers should pass their own + # `summary_content` slot; the built-in title rendering assumes the + # `:default` shape. def initialize(title: nil, align: "right", open: false, variant: :default, **opts) @title = title @align = align.to_sym @open = open - @variant = variant.to_sym + @variant = variant&.to_sym @opts = opts - raise ArgumentError, "Invalid variant: #{@variant}. Must be one of #{VARIANTS.inspect}" unless VARIANTS.include?(@variant) + raise ArgumentError, "Invalid variant: #{@variant.inspect}. Must be one of #{VARIANTS.inspect}" unless VARIANTS.include?(@variant) end def details_classes - case variant + base = case variant when :card "group bg-container p-4 shadow-border-xs rounded-xl" + when :card_inset + "group bg-surface-inset rounded-xl p-4" else "group" end + + class_names(base, opts[:class]) + end + + # `opts` minus the `:class` key, since `details_classes` merges that + # separately to avoid duplicate-keyword collisions when forwarding to + # `tag.details`. + def details_opts + opts.except(:class) end def summary_classes case variant - when :card - # Card variant: no bg on summary — the parent details *is* the + when :card, :card_inset + # Card variants: no bg on summary — the parent details *is* the # surface. Keep cursor + focus-visible ring + flex baseline. # Ring token matches `settings/provider_card.html.erb` (the # established focus pattern on container cards). "list-none cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-alpha-black-300 rounded-xl" + when :inline + # Inline variant: no surface, no padding — the summary reads as + # plain text-link copy. Caller markup (text + optional chevron) + # provides the visual. Keep cursor + focus-visible ring + matching + # alpha-black-300 token used by the card variants for consistency. + "list-none cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-alpha-black-300 rounded-sm" else "px-3 py-2 rounded-xl cursor-pointer flex items-center justify-between bg-surface focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-alpha-black-300" end diff --git a/app/components/DS/pill.rb b/app/components/DS/pill.rb index e3c7ac2b3..efafa67ea 100644 --- a/app/components/DS/pill.rb +++ b/app/components/DS/pill.rb @@ -1,31 +1,60 @@ class DS::Pill < DesignSystemComponent - TONES = %i[violet indigo fuchsia amber green gray].freeze + TONES = %i[violet indigo fuchsia amber green gray red].freeze STYLES = %i[soft filled outline].freeze SIZES = %i[sm md].freeze - attr_reader :label, :tone, :style, :size, :show_dot, :dot_only, :title, :icon + # Semantic-name → visual-tone aliases. Lets callers say + # `tone: :success` instead of binding to the underlying palette name. + # The aliases live here (not on the caller) so the visual palette can + # be retuned without touching every callsite. + SEMANTIC_TONE_ALIASES = { + success: :green, + warning: :amber, + error: :red, + destructive: :red, + info: :indigo, + neutral: :gray + }.freeze - # Generic inline pill primitive. Used for Beta / Canary markers and goal - # status badges, but designed so any future tag (NEW, PRO, EXPERIMENTAL, - # etc.) reuses the same shape without forking. + attr_reader :label, :tone, :style, :size, :show_dot, :dot_only, :title, :icon, :marker + + # Generic inline pill primitive. Two modes: + # + # - `marker: true` (default) — the original shape from #1829: uppercase + # 10/11px text, tracking-wide. Reads as a stage marker (Beta, Canary, + # NEW, PRO, EXPERIMENTAL, …). + # + # - `marker: false` — normal case, snaps to the DS text scale + # (`text-xs` / `text-sm`). Reads as a status / category badge. + # Pair with semantic tones (`:success`, `:warning`, `:error`, + # `:info`, `:neutral`) for status badges; pair with visual tones + # (`:violet`, `:indigo`, etc.) for category tags. + # + # Other options: # # - `dot_only: true` renders only the colored dot (no label, no border). # Use on the collapsed sidebar nav, where there's no room for the label. # - `icon:` overrides the dot with a Lucide icon (sized xs, current color). # Useful for status pills that benefit from a glyph (circle-check, # triangle-alert, pause, etc.) rather than the generic dot. - # - Sure has full violet / indigo / fuchsia / amber / green / gray 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: true, dot_only: false, title: nil, icon: nil) + # - Tones accept both visual names (`:violet`, `:amber`, …) and + # semantic aliases (`:success`, `:warning`, `:error`, + # `:destructive`, `:neutral`, `:info`). Aliases resolve via + # `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: true, dot_only: false, title: nil, icon: nil, marker: true) + 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?(tone.to_sym) ? tone.to_sym : :violet + @tone = TONES.include?(resolved_tone) ? resolved_tone : :violet @style = STYLES.include?(style.to_sym) ? style.to_sym : :soft @size = SIZES.include?(size.to_sym) ? size.to_sym : :sm @show_dot = show_dot @dot_only = dot_only @title = title @icon = icon + @marker = marker end def palette @@ -39,7 +68,8 @@ class DS::Pill < DesignSystemComponent fuchsia: { bg: "var(--color-fuchsia-50)", bg_dark: "var(--color-fuchsia-tint-10)", text: "color-mix(in oklab, var(--color-fuchsia-700), black 30%)", text_dark: "var(--color-fuchsia-200)", border: "var(--color-fuchsia-200)", dot: "var(--color-fuchsia-500)", fill: "var(--color-fuchsia-500)" }, amber: { bg: "var(--color-yellow-50)", bg_dark: "var(--color-yellow-tint-10)", text: "color-mix(in oklab, var(--color-yellow-700), black 30%)", text_dark: "var(--color-yellow-200)", border: "var(--color-yellow-200)", dot: "var(--color-yellow-500)", fill: "var(--color-yellow-500)" }, green: { bg: "var(--color-green-50)", bg_dark: "var(--color-green-tint-10)", text: "color-mix(in oklab, var(--color-green-700), black 30%)", text_dark: "var(--color-green-200)", border: "var(--color-green-200)", dot: "var(--color-green-500)", fill: "var(--color-green-500)" }, - gray: { bg: "var(--color-gray-100)", bg_dark: "var(--color-gray-tint-10)", text: "color-mix(in oklab, var(--color-gray-700), black 30%)", text_dark: "var(--color-gray-200)", border: "var(--color-gray-200)", dot: "var(--color-gray-500)", fill: "var(--color-gray-500)" } + gray: { bg: "var(--color-gray-100)", bg_dark: "var(--color-gray-tint-10)", text: "color-mix(in oklab, var(--color-gray-700), black 30%)", text_dark: "var(--color-gray-200)", border: "var(--color-gray-200)", dot: "var(--color-gray-500)", fill: "var(--color-gray-500)" }, + red: { bg: "var(--color-red-50)", bg_dark: "var(--color-red-tint-10)", text: "color-mix(in oklab, var(--color-red-700), black 30%)", text_dark: "var(--color-red-200)", border: "var(--color-red-200)", dot: "var(--color-red-500)", fill: "var(--color-red-500)" } }[tone] end @@ -77,15 +107,27 @@ class DS::Pill < DesignSystemComponent def container_classes base = [ - "inline-flex items-center align-middle font-medium uppercase whitespace-nowrap shrink-0", + "inline-flex items-center align-middle font-medium whitespace-nowrap shrink-0", "border rounded-md", "leading-none" ] - # text-[10/11px] stays as arbitrary values: the pill is intentionally - # sub-12px (Sure's smallest scale token is text-xs / 12px) to read as - # a marker, not a label. Padding / gap / tracking snap to Tailwind's - # scale to satisfy the design-system "no arbitrary values" rule. - base << (size == :md ? "px-2 py-0.5 text-[11px] tracking-wide gap-1" : "px-1.5 py-0.5 text-[10px] tracking-wider gap-1") + + if marker + # Marker mode (Beta / Canary / NEW): uppercase, sub-12px text, + # wider tracking. text-[10/11px] stays as arbitrary values — the + # pill is intentionally sub-12px (Sure's smallest scale token is + # text-xs / 12px) so it reads as a marker, not a label. Padding / + # gap / tracking snap to Tailwind's scale to satisfy the + # design-system "no arbitrary values" rule. + base << "uppercase" + base << (size == :md ? "px-2 py-0.5 text-[11px] tracking-wide gap-1" : "px-1.5 py-0.5 text-[10px] tracking-wider gap-1") + else + # Badge mode (Pending / Active / Past due / category tag): + # normal case, snaps to the design-system text scale + # (`text-xs` / `text-sm`). Padding bumps slightly so the badge + # reads as a status chip rather than a sub-12px marker. + base << (size == :md ? "px-2 py-0.5 text-sm gap-1.5" : "px-1.5 py-0.5 text-xs gap-1") + end class_names(*base) end end diff --git a/app/views/enable_banking_items/_enable_banking_item.html.erb b/app/views/enable_banking_items/_enable_banking_item.html.erb index a63eaa478..2e3af279b 100644 --- a/app/views/enable_banking_items/_enable_banking_item.html.erb +++ b/app/views/enable_banking_items/_enable_banking_item.html.erb @@ -55,7 +55,7 @@ <% if enable_banking_item.requires_update? %> <%= button_to reauthorize_enable_banking_item_path(enable_banking_item), method: :post, - class: "inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium rounded-lg text-white bg-warning hover:opacity-90 transition-colors", + class: "inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium rounded-lg text-inverse bg-warning hover:opacity-90 transition-colors", data: { turbo: false } do %> <%= icon "refresh-cw", size: "sm" %> <%= t(".update") %> diff --git a/app/views/indexa_capital_items/_indexa_capital_item.html.erb b/app/views/indexa_capital_items/_indexa_capital_item.html.erb index e6535d76a..d03c2ffb1 100644 --- a/app/views/indexa_capital_items/_indexa_capital_item.html.erb +++ b/app/views/indexa_capital_items/_indexa_capital_item.html.erb @@ -9,7 +9,7 @@
<%= icon "chevron-right", class: "group-open:rotate-90 motion-safe:transition-transform motion-safe:duration-150" %> -
+
<%= tag.p indexa_capital_item.name.first.upcase, class: "text-primary text-xs font-medium" %>
diff --git a/app/views/pending_duplicate_merges/new.html.erb b/app/views/pending_duplicate_merges/new.html.erb index 580603f62..4a21bd2a4 100644 --- a/app/views/pending_duplicate_merges/new.html.erb +++ b/app/views/pending_duplicate_merges/new.html.erb @@ -52,7 +52,7 @@
<%= entry.name %>
- <%= I18n.l(entry.date, format: :short) %> • + <%= entry.date ? I18n.l(entry.date, format: :short) : "—" %> • <%= number_to_currency(entry.amount.abs, unit: Money::Currency.new(entry.currency).symbol) %>
diff --git a/app/views/plaid_items/_plaid_item.html.erb b/app/views/plaid_items/_plaid_item.html.erb index 96e621d64..36408d89e 100644 --- a/app/views/plaid_items/_plaid_item.html.erb +++ b/app/views/plaid_items/_plaid_item.html.erb @@ -21,7 +21,7 @@
<%= tag.p plaid_item.name, class: "font-medium text-primary" %> <% if plaid_item.scheduled_for_deletion? %> -

(deletion in progress...)

+

<%= t(".deletion_in_progress") %>

<% end %>
<% if plaid_item.syncing? %> diff --git a/app/views/settings/_section.html.erb b/app/views/settings/_section.html.erb index 0c05833e8..14ef45a61 100644 --- a/app/views/settings/_section.html.erb +++ b/app/views/settings/_section.html.erb @@ -1,35 +1,39 @@ <%# locals: (title:, subtitle: nil, content:, collapsible: false, open: true, auto_open_param: nil, status: nil, meta: nil, actions: nil, badge: nil) %> <% if collapsible %> -
- class="group bg-container shadow-border-xs rounded-xl p-4" - <%= "data-controller=\"auto-open\" data-auto-open-param-value=\"#{h(auto_open_param)}\"".html_safe if auto_open_param.present? %>> - -
- <%= icon "chevron-right", class: "text-secondary group-open:transform group-open:rotate-90 transition-transform" %> -
-
-

<%= title %>

- <%= badge if badge.present? %> + <%= render DS::Disclosure.new( + variant: :card, + open: open, + data: auto_open_param.present? ? { controller: "auto-open", auto_open_param_value: auto_open_param } : nil + ) do |disclosure| %> + <% disclosure.with_summary_content do %> +
+
+ <%= icon "chevron-right", class: "text-secondary group-open:rotate-90 motion-safe:transition-transform motion-safe:duration-150" %> +
+
+

<%= title %>

+ <%= badge if badge.present? %> +
+ <% if subtitle.present? %> +

<%= subtitle %>

+ <% end %>
- <% if subtitle.present? %> -

<%= subtitle %>

- <% end %>
+ <% if status.present? %> +
+ <% if meta.present? %> + <%= meta %> + <% end %> + <%= status %> + <%= actions if actions.present? %> +
+ <% end %>
- <% if status.present? %> -
- <% if meta.present? %> - <%= meta %> - <% end %> - <%= status %> - <%= actions if actions.present? %> -
- <% end %> -
+ <% end %>
<%= content %>
-
+ <% end %> <% else %>
diff --git a/app/views/settings/debugs/show.html.erb b/app/views/settings/debugs/show.html.erb index 252ec4ed5..7d7d6c8b5 100644 --- a/app/views/settings/debugs/show.html.erb +++ b/app/views/settings/debugs/show.html.erb @@ -59,7 +59,7 @@ <% if @debug_log_entries.any? %>
- + @@ -70,7 +70,7 @@ - + <% @debug_log_entries.each do |entry| %> diff --git a/app/views/settings/providers/_ibkr_panel.html.erb b/app/views/settings/providers/_ibkr_panel.html.erb index 578c6af34..7e9e686d9 100644 --- a/app/views/settings/providers/_ibkr_panel.html.erb +++ b/app/views/settings/providers/_ibkr_panel.html.erb @@ -13,17 +13,17 @@ t(".steps.step_5") ] %> -
- + <%= render DS::Disclosure.new(variant: :card_inset) do |disclosure| %> + <% disclosure.with_summary_content do %>

<%= t(".flex_query_details.eyebrow") %>

<%= t(".flex_query_details.title") %>

<%= t(".flex_query_details.summary") %>

- <%= icon "chevron-down", class: "mt-0.5 text-secondary transition-transform group-open:rotate-180" %> + <%= icon "chevron-down", class: "mt-0.5 text-secondary group-open:rotate-180 motion-safe:transition-transform motion-safe:duration-150" %>
-
+ <% end %>
@@ -89,7 +89,7 @@

<%= t(".report_window_note") %>

-
+ <% end %> <% ibkr_item = Current.family.ibkr_items.first_or_initialize(name: "Interactive Brokers") diff --git a/app/views/settings/providers/_indexa_capital_panel.html.erb b/app/views/settings/providers/_indexa_capital_panel.html.erb index c751e0feb..255126e42 100644 --- a/app/views/settings/providers/_indexa_capital_panel.html.erb +++ b/app/views/settings/providers/_indexa_capital_panel.html.erb @@ -29,10 +29,12 @@ type: :password %>

<%= t("indexa_capital_items.panel.fields.api_token.description") %>

-
- - <%= t("indexa_capital_items.panel.alternative_auth") %> - + <%= render DS::Disclosure.new(variant: :inline) do |disclosure| %> + <% disclosure.with_summary_content do %> + + <%= t("indexa_capital_items.panel.alternative_auth") %> + + <% end %>
<%= form.text_field :username, label: t("indexa_capital_items.panel.fields.username.label"), @@ -49,7 +51,7 @@ placeholder: is_new_record ? t("indexa_capital_items.panel.fields.password.placeholder_new") : t("indexa_capital_items.panel.fields.password.placeholder_update"), type: :password %>
-
+ <% end %>
<%= form.submit is_new_record ? t("indexa_capital_items.panel.save_button") : t("indexa_capital_items.panel.update_button"), diff --git a/app/views/settings/providers/_snaptrade_panel.html.erb b/app/views/settings/providers/_snaptrade_panel.html.erb index ec877bdcd..1c814b234 100644 --- a/app/views/settings/providers/_snaptrade_panel.html.erb +++ b/app/views/settings/providers/_snaptrade_panel.html.erb @@ -57,25 +57,31 @@ <% if items&.any? && items.first.user_registered? %> <% item = items.first %>
-
- -
-

- <%= t("providers.snaptrade.status_connected", count: item.snaptrade_accounts.count) %> - <% if item.unlinked_accounts_count > 0 %> - (<%= t("providers.snaptrade.needs_setup", count: item.unlinked_accounts_count) %>) - <% end %> -

+ <%= render DS::Disclosure.new( + variant: :inline, + data: { + controller: "lazy-load", + action: "toggle->lazy-load#toggled", + lazy_load_url_value: connections_snaptrade_item_path(item), + lazy_load_auto_open_param_value: "manage" + } + ) do |disclosure| %> + <% disclosure.with_summary_content do %> +
+
+

+ <%= t("providers.snaptrade.status_connected", count: item.snaptrade_accounts.count) %> + <% if item.unlinked_accounts_count > 0 %> + (<%= t("providers.snaptrade.needs_setup", count: item.unlinked_accounts_count) %>) + <% end %> +

+
+ + <%= t("providers.snaptrade.manage_connections") %> + <%= icon "chevron-right", class: "w-3 h-3 group-open:rotate-90 motion-safe:transition-transform motion-safe:duration-150" %> +
- - <%= t("providers.snaptrade.manage_connections") %> - <%= icon "chevron-right", class: "w-3 h-3 transition-transform group-open:rotate-90" %> - -
+ <% end %>
@@ -86,7 +92,7 @@
-
+ <% end %>
<% end %>
diff --git a/app/views/trades/_header.html.erb b/app/views/trades/_header.html.erb index 94e8a2c5a..eea444354 100644 --- a/app/views/trades/_header.html.erb +++ b/app/views/trades/_header.html.erb @@ -31,7 +31,7 @@ - <%= I18n.l(entry.date, format: :long) %> + <%= entry.date ? I18n.l(entry.date, format: :long) : "—" %> <% end %> diff --git a/app/views/transactions/_header.html.erb b/app/views/transactions/_header.html.erb index d80c13bed..a5ca70b63 100644 --- a/app/views/transactions/_header.html.erb +++ b/app/views/transactions/_header.html.erb @@ -19,7 +19,7 @@
- <%= I18n.l(entry.date, format: :long) %> + <%= entry.date ? I18n.l(entry.date, format: :long) : "—" %> <% if entry.transaction.pending? %> "> diff --git a/app/views/valuations/_header.html.erb b/app/views/valuations/_header.html.erb index d4a376876..2a8566273 100644 --- a/app/views/valuations/_header.html.erb +++ b/app/views/valuations/_header.html.erb @@ -19,6 +19,6 @@
- <%= I18n.l(entry.date, format: :long) %> + <%= entry.date ? I18n.l(entry.date, format: :long) : "—" %> <% end %> diff --git a/config/locales/views/plaid_items/en.yml b/config/locales/views/plaid_items/en.yml index 0dbe8ee51..25e87511c 100644 --- a/config/locales/views/plaid_items/en.yml +++ b/config/locales/views/plaid_items/en.yml @@ -15,6 +15,7 @@ en: connection_lost_description: This connection is no longer valid. You'll need to delete this connection and add it again to continue syncing data. delete: Delete + deletion_in_progress: (deletion in progress...) error: Error occurred while syncing data no_accounts_description: We could not load any accounts from this financial institution. diff --git a/test/components/DS/pill_test.rb b/test/components/DS/pill_test.rb new file mode 100644 index 000000000..f108593b4 --- /dev/null +++ b/test/components/DS/pill_test.rb @@ -0,0 +1,59 @@ +require "test_helper" + +class DS::PillTest < ViewComponent::TestCase + test "marker mode (default) renders uppercase sub-12px chrome" do + render_inline(DS::Pill.new(label: "Beta", tone: :violet)) + + pill = page.find("span", text: "Beta") + assert_includes pill[:class], "uppercase" + # Marker keeps sub-12px text via arbitrary value (intentional — see component docs). + assert_match(/text-\[1[01]px\]/, pill[:class]) + end + + test "marker: false renders normal-case DS-scale chrome" do + render_inline(DS::Pill.new(label: "Active", tone: :success, marker: false)) + + pill = page.find("span", text: "Active") + refute_includes pill[:class], "uppercase" + # Badge mode snaps to text-xs / text-sm — no sub-12px arbitrary values. + assert_match(/text-(xs|sm)/, pill[:class]) + refute_match(/text-\[1[01]px\]/, pill[:class]) + end + + test "semantic tone aliases resolve to visual palette tones" do + { + success: :green, + warning: :amber, + error: :red, + destructive: :red, + info: :indigo, + neutral: :gray + }.each do |alias_name, expected_visual| + pill = DS::Pill.new(label: "x", tone: alias_name) + assert_equal expected_visual, pill.tone, "Expected #{alias_name} → #{expected_visual}, got #{pill.tone}" + end + end + + test "unknown tone falls back to violet" do + pill = DS::Pill.new(label: "x", tone: :nonexistent) + assert_equal :violet, pill.tone + end + + test "red tone palette resolves to red-* tokens" do + pill = DS::Pill.new(label: "Failed", tone: :error) + assert_includes pill.palette[:dot], "color-red-500" + assert_includes pill.palette[:bg], "color-red-50" + end + + test "icon option renders glyph in place of dot" do + render_inline(DS::Pill.new(label: "Syncing", tone: :info, marker: false, icon: "loader")) + + # Lucide icon helper renders the inline SVG; verifying we see at least one + # is enough — the icon helper is covered by its own tests. + assert_selector "svg" + # And the dot is suppressed when an icon takes its place. `refute_selector + # ..., count: N` only fails when there are exactly N matches, so use + # `assert_no_selector` to strictly assert zero dots. + assert_no_selector "span.rounded-full[style*='background-color']" + end +end diff --git a/test/components/previews/pill_component_preview.rb b/test/components/previews/pill_component_preview.rb index 88a7aa93f..ae036179c 100644 --- a/test/components/previews/pill_component_preview.rb +++ b/test/components/previews/pill_component_preview.rb @@ -1,26 +1,68 @@ class PillComponentPreview < ViewComponent::Preview - # @param tone select ["violet", "indigo", "fuchsia", "amber", "gray"] + # @param tone select ["violet", "indigo", "fuchsia", "amber", "green", "gray", "red", "success", "warning", "error", "info", "neutral"] # @param style select ["soft", "filled", "outline"] # @param size select ["sm", "md"] # @param label text # @param show_dot toggle # @param dot_only toggle - def default(tone: "violet", style: "soft", size: "sm", label: "Preview", show_dot: true, dot_only: false) + # @param marker toggle + # @param icon text + def default(tone: "violet", style: "soft", size: "sm", label: "Preview", show_dot: true, dot_only: false, marker: true, icon: nil) render DS::Pill.new( label: label, tone: tone.to_sym, style: style.to_sym, size: size.to_sym, show_dot: show_dot, - dot_only: dot_only + dot_only: dot_only, + marker: marker, + icon: icon.presence ) end + # @!group Stage markers (marker: true — original #1829 shape) def canary render DS::Pill.new(label: "Canary", tone: :fuchsia) end + def beta + render DS::Pill.new(label: "Beta", tone: :violet) + end + + def new_marker + render DS::Pill.new(label: "New", tone: :indigo) + end + def dot_only_collapsed_sidebar render DS::Pill.new(dot_only: true, tone: :violet) end + # @!endgroup + + # @!group Status badges (marker: false, semantic tones) + def status_active + render DS::Pill.new(label: "Active", tone: :success, marker: false) + end + + def status_pending + render DS::Pill.new(label: "Pending", tone: :warning, marker: false) + end + + def status_failed + render DS::Pill.new(label: "Failed", tone: :error, marker: false, icon: "circle-alert") + end + + def status_archived + render DS::Pill.new(label: "Archived", tone: :neutral, marker: false) + end + + def status_info + render DS::Pill.new(label: "Syncing", tone: :info, marker: false, icon: "loader") + end + # @!endgroup + + # @!group Sizes (md) + def status_md + render DS::Pill.new(label: "Past due", tone: :error, marker: false, size: :md) + end + # @!endgroup end
<%= t(".table.time") %> <%= t(".table.level") %><%= t(".table.metadata") %>
<%= l(entry.created_at, format: :long) %>