diff --git a/app/components/DS/pill.html.erb b/app/components/DS/pill.html.erb
index 6712f0602..2dc15e0ec 100644
--- a/app/components/DS/pill.html.erb
+++ b/app/components/DS/pill.html.erb
@@ -9,7 +9,9 @@
title="<%= title || t("ds.pill.aria_label", label: label) %>">
<% else %>
- <% if show_dot %>
+ <% if icon %>
+ <%= helpers.icon(icon, size: "xs", color: "current") %>
+ <% elsif show_dot %>
<% end %>
diff --git a/app/components/DS/pill.rb b/app/components/DS/pill.rb
index 05733c4f4..e3c7ac2b3 100644
--- a/app/components/DS/pill.rb
+++ b/app/components/DS/pill.rb
@@ -1,20 +1,23 @@
class DS::Pill < DesignSystemComponent
- TONES = %i[violet indigo fuchsia amber gray].freeze
+ TONES = %i[violet indigo fuchsia amber green gray].freeze
STYLES = %i[soft filled outline].freeze
SIZES = %i[sm md].freeze
- attr_reader :label, :tone, :style, :size, :show_dot, :dot_only, :title
+ attr_reader :label, :tone, :style, :size, :show_dot, :dot_only, :title, :icon
- # Generic inline pill primitive. Currently the home of Beta / Canary
- # markers; can be reused for future tags (NEW, PRO, EXPERIMENTAL, etc.)
- # without forking the component.
+ # 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.
#
# - `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.
- # - Sure has full violet / indigo / fuchsia / amber / 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:` 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)
@label = label || I18n.t("ds.pill.default_label", default: "Beta")
@tone = TONES.include?(tone.to_sym) ? tone.to_sym : :violet
@style = STYLES.include?(style.to_sym) ? style.to_sym : :soft
@@ -22,6 +25,7 @@ class DS::Pill < DesignSystemComponent
@show_dot = show_dot
@dot_only = dot_only
@title = title
+ @icon = icon
end
def palette
@@ -34,6 +38,7 @@ class DS::Pill < DesignSystemComponent
indigo: { bg: "var(--color-indigo-50)", bg_dark: "var(--color-indigo-tint-10)", text: "color-mix(in oklab, var(--color-indigo-700), black 30%)", text_dark: "var(--color-indigo-200)", border: "var(--color-indigo-200)", dot: "var(--color-indigo-500)", fill: "var(--color-indigo-500)" },
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)" }
}[tone]
end
diff --git a/app/components/goals/status_pill_component.html.erb b/app/components/goals/status_pill_component.html.erb
index 548a0b37a..e2f64df78 100644
--- a/app/components/goals/status_pill_component.html.erb
+++ b/app/components/goals/status_pill_component.html.erb
@@ -1,4 +1 @@
-
- <%= helpers.icon(icon_name, size: "xs", color: "current") %>
- <%= label %>
-
+<%= render DS::Pill.new(label: label, tone: variant[:tone], style: :outline, icon: variant[:icon]) %>
diff --git a/app/components/goals/status_pill_component.rb b/app/components/goals/status_pill_component.rb
index d1ec2179f..134500b62 100644
--- a/app/components/goals/status_pill_component.rb
+++ b/app/components/goals/status_pill_component.rb
@@ -1,19 +1,17 @@
class Goals::StatusPillComponent < ApplicationComponent
- # Text colors here intentionally use palette steps (green/yellow/gray-700)
- # instead of the `text-success` / `text-warning` / `text-secondary` tokens
- # because the functional tokens drop below WCAG 1.4.3 4.5:1 on tinted
- # surfaces in light mode (~2.88:1 / 3.0:1 / 4.16:1). Each variant carries
- # a theme-dark: override so the dark-700 text doesn't disappear against
- # the dark-mode tinted surface. Local override only; revert once
- # we-promise/sure#1736 lands token-level fixes.
+ # Maps the goal's display_status to the DS::Pill primitive's tone +
+ # glyph. Outline style is used so the pill keeps its colored border on
+ # any card background (resting bg-container, hover bg-surface-hover);
+ # the filled / soft variants blended into the hover state and lost
+ # contrast on cards.
VARIANTS = {
- on_track: { classes: "bg-green-500/10 text-green-700 theme-dark:text-green-300", icon: "circle-check" },
- behind: { classes: "bg-surface-inset text-yellow-700 theme-dark:text-yellow-300", icon: "triangle-alert" },
- reached: { classes: "bg-green-500/10 text-green-700 theme-dark:text-green-300", icon: "star" },
- completed: { classes: "bg-green-500/10 text-green-700 theme-dark:text-green-300", icon: "circle-check-big" },
- no_target_date: { classes: "bg-surface-inset text-gray-700 theme-dark:text-gray-200", icon: "infinity" },
- paused: { classes: "bg-surface-inset text-gray-700 theme-dark:text-gray-200", icon: "pause" },
- archived: { classes: "bg-surface-inset text-gray-700 theme-dark:text-gray-200", icon: "archive" }
+ on_track: { tone: :green, icon: "circle-check" },
+ behind: { tone: :amber, icon: "triangle-alert" },
+ reached: { tone: :green, icon: "star" },
+ completed: { tone: :green, icon: "circle-check-big" },
+ no_target_date: { tone: :gray, icon: "infinity" },
+ paused: { tone: :gray, icon: "pause" },
+ archived: { tone: :gray, icon: "archive" }
}.freeze
def initialize(goal:)
@@ -31,12 +29,4 @@ class Goals::StatusPillComponent < ApplicationComponent
def label
I18n.t("goals.status.#{status_key}", default: status_key.to_s.titleize)
end
-
- def classes
- variant[:classes]
- end
-
- def icon_name
- variant[:icon]
- end
end