Lower half of the goal detail used to be: (stat row: monthly pace +
total contributions) + (bottom row: contributions list + funding
breakdown card). Two of those four pieces were redundant:
- Total Contributions stat duplicated the count badge that already
sits beside the Contributions heading below.
- Monthly Pace stat repeated the same numbers the catch-up alert
surfaces above and the chart subtitle reads.
Adopt the dashboard Balance Sheet pattern (app/views/pages/dashboard/_
balance_sheet.html.erb) for the funding widget: inline header with
total ("Funding accounts · $13,250"), thin gap-separated segment bar,
color-dot legend with percent, and a bg-container-inset table with the
shared `pages/dashboard/group_weight` 5-stick weight indicator + value
column.
New show.html.erb bottom: just two full-width sections — funding
widget, then chronological contributions list. Both rendered only when
the goal has contributions (matches the empty-state branch added
earlier).
Locale: goals.show.funding_table.{name, weight, value}.
The "Add $1,531.25" CTA used to open the contribution modal with an
empty amount field — label was a hint, not a default. Now passes the
catch-up amount via ?amount= and the contributions controller seeds
@contribution.amount from params. One click brings the user to the
modal already populated.
Adds a secondary text link below the primary CTA: "Or adjust your
target" → opens the edit modal (Turbo frame). Behavioural-econ choice
architecture: gives the rebaseline path explicitly so users who can't
realistically catch up don't feel forced into the contribution.
Trade-off: lets the alert respect autonomy — commit or recalibrate,
both fine. Action paralysis kept low by visual hierarchy (primary
button vs muted text link).
Alert previous pass led with delta ("Behind by $750/mo") but the user
still had to reconcile that with the $1,000/mo CTA — the relationship
between current pace, gap, and required rate was implicit.
Make every number visible in the sentence:
- Title: "Save $1,000/mo to stay on track" — leads with the action +
required rate. Reduces decision load: the headline is what to do.
- Body: "You're saving $250/mo today — $750/mo short of the pace to
finish by September 11, 2026." — current pace + gap + deadline.
User can now mentally verify: $250 + $750 = $1,000. The catch-up
amount in title + body + CTA is no longer disconnected from the
current pace number; the body is the bridge.
Adds `scrollbar` utility (defined in app/assets/tailwind/application.css
as 4px gray-300 thumb) to the contributions list container. Browser-
default scrollbar was rendering as a thick dark bar in light mode on
some OSes; the in-house utility renders a thin gray thumb consistently
across themes.
- 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.
Empty state (goal with zero contributions) was rendering a flat-at-$0
saved series and a flat-at-$0 projection that looked broken to anyone
opening a freshly created goal. Now show.html.erb branches on
@goal.goal_contributions.empty? and renders a piggy-bank + "Add a
contribution" CTA card before the chart card. Brand-new goals get a
clean inline call-to-action instead of a misleading line at zero.
No-target-date goals (target_amount set, target_date null) used to
render a standalone "Set a target date" prompt card and hide history
entirely. Now they render the chart with the saved history + the
target horizontal line (no projection segment, no projection legend
item), plus a secondary "Set target date" callout below the chart
linking into the edit modal. History is informative even without a
deadline.
Locale: new goals.show.empty.{heading,body,cta}.
A — Catch-up math triple-encoding. The catch-up amount (e.g. $1,531/mo)
was rendered verbatim in three places: the banner title, the projection
card subtitle, and the pace stat ("target $X/mo"). Only new information
anywhere was the buried "Behind by $Y/mo" delta in pace-card subtext.
- Banner body now carries the delta: "...currently $Y/mo behind."
- Projection sentence drops the "Bump to %{required}/mo" restatement;
reduces to "At %{current}/mo you'll miss your target date." Chart
aria-description benefits from the simpler phrasing too.
- Pace stat drops the "· target $X/mo" sub-line. Pair becomes
"$avg/mo" + "Behind by $delta/mo" — same delta now in the banner,
surfaced twice intentionally (alert vs at-a-glance stat).
K — Destructive transition confirms. Pause / Resume stay no-confirm
(recoverable). Mark complete (irreversible via UI; no may_uncomplete?
event exists) and Archive (goal disappears from active list) now wear
CustomConfirm. New locale keys: goals.show.confirm_{complete,archive}_
{title,body,cta}.
Locale catch_up body strings now interpolate %{delta} alongside %{date};
projection.behind drops %{required}. Controller#projection_summary still
passes both keys — extras are ignored by I18n.