- progress_ring_component: drop the in-ring "$saved / of $target" lines.
The same money pair already renders directly below the ring in
show.html.erb (now the single source). Inside the ring keeps only
"Saved" + percent.
- show.html.erb: replace 3 hand-rolled button_to CTAs (Paused banner
"Resume goal", Archived banner "Restore goal", celebration card
"Archive goal") with DS::Button so focus/hover/disabled match the
rest of the app. variant: primary/outline, size: sm, method: :patch.
- status_pill_component: swap text-success / text-warning / text-secondary
to text-green-700 / text-yellow-700 / text-gray-700 so all 5 light-mode
pill variants pass WCAG 4.5:1. Local override pending the upstream DS
token fix tracked at we-promise/sure#1736.
- Projection chart SVG: role=img + <title> + <desc> wired through new
ariaLabelValue / ariaDescriptionValue Stimulus values. Show.html.erb
passes a localized chart label and a strip_tags'd projection summary.
- Progress ring container: role=progressbar + aria-valuenow/min/max +
aria-label so screen readers announce "Goal 27% complete. $13,250 of
$50,000 saved." instead of four disjoint spans.
- Funding-account checkboxes (stepper step 1): explicit per-account id
("savings_goal_account_ids_<id>") so each row has a unique DOM id;
duplicate-id HTML violation gone.
- show.html.erb: <h3> -> <h2> at six section headings (celebration,
no-target-date, projection, contributions, funding accounts, notes)
so the heading hierarchy is h1 -> h2, not h1 -> h3.
- goal_avatar + account_stack components: aria-hidden=true on the
decorative wrappers; the textual goal/account name beside them is
always read separately so the SR no longer prefixes every entry with
the avatar initial.
- New locale keys: savings_goals.show.ring.aria_label and
savings_goals.show.projection.aria_label.
Ring percentage no longer takes the warning yellow tint when behind —
the colored ring stroke + status pill + catch-up alert already signal
the state, doubling it on the percent number was noise. Reached stays
green (celebratory), everything else uses text-primary (white/dark).
Chart vertical line at the left edge was the (start_date, $0) point
the controller prepended to the saved series. When start_date equals
the first contribution date (now common after the earlier earliest-
contribution fix), this drew a vertical jump from $0 to first
contribution at x=start. Skip the prepend when there's no temporal
gap so the line starts at the first real point.
Add Contribution modal — wrap the source-account select in the styled
form-field via f.select instead of label_tag + bare select_tag. Match
the rest of Sure's form controls. Also pass hide_currency on the
amount field so single-currency families don't see a redundant USD
dropdown.
Account avatar colors — replace Ruby String#hash (randomized per
process by Ruby for DoS protection) with a deterministic MD5-based
pick from Savings::GoalAvatarComponent::PALETTE. Same account name
now resolves to the same color across processes and across
components. Apply via a new Savings::GoalAvatarComponent.color_for
helper used by both the form stepper account list and the goal-card
AccountStackComponent (which was hardcoding blue-500 for every avatar
in the stack, hence Chase + Ally looking identical on the wedding
card).
Previous savings goals UI looked nothing like the Claude Design output
(see sure-design-context/design/savings-goals/project/goals/*.jsx) and
the hand-rolled ring did not match the segmented D3 donut used at
app/views/budgets/_budget_donut.html.erb. This rewires the surface end
to end.
Donut chart:
- SavingsGoal#to_donut_segments_json returns the same segment shape as
Budget#to_donut_segments_json: filled portion in goal color, unused
remainder as `var(--budget-unallocated-fill)`. Visual identity is now
the same: segmented arc with cornerRadius and gap, courtesy of the
shared `donut-chart` Stimulus controller and D3.
- ProgressRingComponent renders a `data-controller="donut-chart"` div
with the same default-content/inner-text pattern as `_budget_donut`.
Index page (matches GoalsIndex.jsx):
- Page header: title + "Save toward what matters." subtitle + "New goal"
primary CTA right-aligned.
- Summary strip card: total saved / target, overall bar, active goals,
on-track ratio, behind count.
- State filter rendered as DS::Tabs-style pill nav (`bg-surface-inset
p-1 rounded-lg`, white-pill active state).
- Cards rebuilt: avatar (44px, rounded-xl, white initial on goal color)
+ name + secondary line ("N days left · by date" / "No target date" /
"Completed" / "Past due"), status pill with leading dot, big
$current/$target line + percent, bar in status colour, AccountStack
(overlapping initials) + "N accounts" + "to go".
Goal detail (matches GoalDetail.jsx):
- Header: 64px avatar + h1 name + status pill + "Target $X by date ·
N days left" subline + Edit (outline) + Add contribution (primary) +
kebab (DS::Menu for AASM transitions).
- Donut-chart ring card with stats overlay.
- 4-col stat row (Avg monthly, Total contributions, Target date,
Started) with mono numerals and "Needs $X/mo" / "Above target pace"
sub-captions where relevant.
- Two-col bottom: contributions list (avatar + account · date · source
· green +$amount) and funding accounts breakdown (stacked bar +
per-account row with $ and % of saved).
New components: Savings::AccountStackComponent (overlapping account
initials with ring-2 ring-container). StatusPillComponent now uses a
leading colored dot instead of an icon. GoalAvatarComponent radii
match Claude Design (rounded-md/lg/xl/2xl) and white initial.
Locale: new keys under savings_goals.{index.subtitle, index.summary.*,
goal_card.{accounts,days_left,completed,past_due,no_target_date},
show.header.*, show.ring.{of,to_go}, show.stats.*, show.funding_balance,
show.of_saved, show.notes}.
- StatusPill: use functional `text-success` / `text-warning` tokens with
matching icon colors and `px-2 py-1`, mirroring
`app/views/budget_categories/_budget_category.html.erb:29-43`.
- ProgressRing: rework center text to match `_budget_donut.html.erb`
(small "Saved" label, `text-3xl font-medium` headline, "of $X"
underline). Stroke color now derives from goal.status (yellow when
behind, blue on track, green reached, gray for no-date).
- GoalCard bar: track height + transition match budget category bar
(`h-1.5`, `transition-all duration-500`, `inline-size`).
- Index/show layouts: render page header inline (`<h1>` + actions). The
default application layout doesn't yield `:page_actions`, so the
CTA + kebab menu wouldn't appear when emitted via `content_for`.
- Stepper review summary: target the actual form inputs by `name`
rather than relying on the `data-target` Stimulus attribute, since
`money_field` puts the attribute on the wrapper. Step 1 validation
scoped to the step 1 panel.
- Demo generator: filter Depository accounts via
`where(accountable_type: "Depository")` — Rails delegated_type
generates the `depository?` predicate, not a `.depository` scope.
Adds a standalone Savings goals feature: a piggy-bank style tracker that
lets a family set a target, link one or more Depository accounts as
funding sources, and log manual contributions over time. Supersedes #1569
(closed) — same intent, redesigned per reviewer + Discord feedback.
What this adds:
- New `/savings_goals` sidebar entry (piggy-bank icon) with index, show,
state-filtered tabs (all/active/paused/completed/archived), and a
2-step modal stepper for creation (Identity → Review).
- Multi-account funding via a `SavingsGoalAccount` join: a goal requires
≥1 linked Depository account (checking/savings/HSA/CD/money-market),
and all linked accounts must share the goal's currency.
- Tracker balance model: goal balance = SUM(contributions.amount). No
auto-flow from account balances. Contributions are pure logical
records and don't move money between accounts.
- Manual contributions modal scoped to the goal's linked accounts.
Initial contributions seeded at creation can't be deleted; manual
ones can.
- AASM lifecycle: active / paused / completed / archived.
Hard-delete only after archive.
- Status pills (On track / Behind / Reached / No date) derived from
pace vs target_date.
- AI Assistant tool `create_savings_goal` lets the sidebar chat create
a goal end-to-end from a natural-language prompt; soft errors carry
the available-accounts list back to the LLM (mirrors the existing
`import_bank_statement` pattern).
- Family-scoped throughout (`Current.family`-only access, account
family-scoping enforced both in controllers and the AI tool).
- Demo data seed wires up 4 sample goals across the Depository accounts.
Intentionally out of scope (separate PRs / v1.1):
- Auto-fund from budget surplus + Sidekiq cron + budget-show card.
- Dashboard "Savings goals" widget.
- "Behind pace" projection chart on the detail page.
- `evaluate_savings_goal_feasibility` LLM tool (level-setting before
create_savings_goal).
- Spend-less goals inside Budgets.
- Family-member-private goals (deferred investigation).