ux(goals): catch-up rework, dark-mode pill contrast, color disclosure, stepper continue-right

- catch_up alert: title now leads with the new info (delta) and body
  states the required rate. Was "Save $1,000/mo to catch up" + "Currently
  $750/mo behind" — confusingly double-stated. Now "Behind by $750/mo" +
  "Save $1,000/mo to stay on track for {date}." Locale keys swap the
  %{amount}/%{delta} placement.

- Goals::StatusPillComponent: each variant carries a theme-dark: text
  override so the dark-700 text doesn't disappear against the dark-mode
  tinted surface. Verified in dark mode: Paused pill text is now
  rgb(231,231,231) (gray-200) instead of rgb(54,54,54) (gray-700).
  Pre-existing token contrast fix tracked at we-promise/sure#1736 stays
  the long-term path; this is the local workaround that doesn't drop
  4.5:1 in either theme.

- New goals/_color_picker.html.erb partial: <details> disclosure with
  current-color preview in the summary + swatch grid in the popover.
  Mirrors the categories form's pen-icon-overlay pattern in spirit
  (collapsed by default; user clicks to expand). Both _form_edit and
  _form_stepper render the partial; the stepper's hidden color field is
  replaced by the visible disclosure.

- Stepper footer: change `justify-between` to `flex items-center` plus
  `ml-auto` on the Continue wrapper. Continue now sits right-aligned in
  step 1 (where Back is hidden) and stays right in step 2 with Back
  taking the left edge.
This commit is contained in:
Guillem Arias
2026-05-11 21:00:47 +02:00
parent f6108e2a7b
commit b47e3478b7
6 changed files with 50 additions and 41 deletions

View File

@@ -1,16 +1,18 @@
class Goals::StatusPillComponent < ApplicationComponent
# Text colors here intentionally use palette steps (green-700 / yellow-700 /
# gray-700) rather than `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). Local override only;
# revert once we-promise/sure#1736 lands token-level fixes.
# 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.
VARIANTS = {
on_track: { classes: "bg-green-500/10 text-green-700", icon: "circle-check" },
behind: { classes: "bg-yellow-500/10 text-yellow-700", icon: "triangle-alert" },
reached: { classes: "bg-green-500/10 text-green-700", icon: "star" },
no_target_date: { classes: "bg-surface-inset text-gray-700", icon: "infinity" },
paused: { classes: "bg-surface-inset text-gray-700", icon: "pause" },
archived: { classes: "bg-surface-inset text-gray-700", icon: "archive" }
on_track: { classes: "bg-green-500/10 text-green-700 theme-dark:text-green-300", icon: "circle-check" },
behind: { classes: "bg-yellow-500/10 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" },
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" }
}.freeze
def initialize(goal:)

View File

@@ -0,0 +1,16 @@
<%# locals: (form:, colors:) %>
<details class="relative">
<summary class="flex items-center gap-2 cursor-pointer text-sm text-secondary list-none">
<span class="block w-5 h-5 rounded-full" style="background-color: <%= form.object.color %>"></span>
<span><%= t("goals.form_stepper.step1.fields.color") %></span>
<%= icon("chevron-down", size: "sm") %>
</summary>
<div class="absolute z-10 mt-2 p-3 bg-container border border-alpha-black-25 rounded-xl shadow-border-xs flex flex-wrap gap-2 w-fit">
<% colors.each do |c| %>
<label class="relative">
<%= form.radio_button :color, c, class: "sr-only peer" %>
<span class="block w-6 h-6 rounded-full cursor-pointer peer-checked:ring-2 peer-checked:ring-offset-2 peer-checked:ring-alpha-black-300" style="background-color: <%= c %>"></span>
</label>
<% end %>
</div>
</details>

View File

@@ -50,18 +50,7 @@
</div>
</div>
<div>
<span class="block text-sm text-secondary mb-2"><%= t("goals.form_stepper.step1.fields.color") %></span>
<div class="flex flex-wrap gap-2">
<% Goal::COLORS.each do |c| %>
<label class="relative">
<%= f.radio_button :color, c, class: "sr-only peer" %>
<div class="w-6 h-6 rounded-full cursor-pointer peer-checked:ring-2 peer-checked:ring-offset-2 peer-checked:ring-gray-500"
style="background-color: <%= c %>"></div>
</label>
<% end %>
</div>
</div>
<%= render "color_picker", form: f, colors: Goal::COLORS %>
<%= f.text_area :notes,
label: t("goals.form_stepper.step1.fields.notes"),

View File

@@ -104,7 +104,7 @@
placeholder: t("goals.form_stepper.step1.fields.notes_placeholder") %>
<% end %>
<%= f.hidden_field :color %>
<%= render "color_picker", form: f, colors: Goal::COLORS %>
</section>
<section data-goal-stepper-target="step2Panel" class="space-y-5 hidden">
@@ -155,7 +155,7 @@
</details>
</section>
<div class="flex items-center justify-between pt-2">
<div class="flex items-center pt-2">
<div class="hidden" data-goal-stepper-target="footerLeftButton">
<%= render DS::Button.new(
variant: "ghost",
@@ -167,16 +167,18 @@
}
) %>
</div>
<%= render DS::Button.new(
text: t("goals.form_stepper.continue"),
variant: "primary",
icon: "arrow-right",
icon_position: :right,
data: {
goal_stepper_target: "footerRightButton",
action: "click->goal-stepper#footerRight"
}
) %>
<div class="ml-auto">
<%= render DS::Button.new(
text: t("goals.form_stepper.continue"),
variant: "primary",
icon: "arrow-right",
icon_position: :right,
data: {
goal_stepper_target: "footerRightButton",
action: "click->goal-stepper#footerRight"
}
) %>
</div>
<button type="submit"
class="sr-only"
tabindex="-1"

View File

@@ -137,12 +137,12 @@
<% catch_up_money = Money.new(@goal.monthly_target_amount, @goal.currency) %>
<% catch_up_delta = @goal.monthly_target_amount.to_d - @stats[:avg_monthly].to_d %>
<% catch_up_delta_money = Money.new(catch_up_delta, @goal.currency) %>
<%= render DS::Alert.new(variant: "warning", title: t("goals.show.catch_up.title", amount: catch_up_money.format)) do %>
<%= render DS::Alert.new(variant: "warning", title: t("goals.show.catch_up.title", delta: catch_up_delta_money.format)) do %>
<p class="text-secondary">
<% if @goal.target_date %>
<%= t("goals.show.catch_up.body_with_date", date: I18n.l(@goal.target_date, format: :long), delta: catch_up_delta_money.format) %>
<%= t("goals.show.catch_up.body_with_date", amount: catch_up_money.format, date: I18n.l(@goal.target_date, format: :long)) %>
<% else %>
<%= t("goals.show.catch_up.body", delta: catch_up_delta_money.format) %>
<%= t("goals.show.catch_up.body", amount: catch_up_money.format) %>
<% end %>
</p>
<div class="mt-2">

View File

@@ -123,9 +123,9 @@ en:
on_track: At your current pace, you'll reach this goal around <strong class="text-primary">%{date}</strong>.
aria_label: "Projection chart for %{name}"
catch_up:
title: "Save %{amount}/mo to catch up"
body_with_date: "Bump your monthly contribution to stay on track for %{date}. Currently %{delta}/mo behind."
body: "Bump your monthly contribution to stay on track. Currently %{delta}/mo behind."
title: "Behind by %{delta}/mo"
body_with_date: "Save %{amount}/mo to stay on track for %{date}."
body: "Save %{amount}/mo to stay on track."
cta: "Add %{amount}"
confirm_complete_title: Mark this goal complete?
confirm_complete_body: It leaves the Ongoing list. You can still archive or restore it later.