Same pattern as the bank-providers page's `AVAILABLE · 3` header (see
`app/views/settings/providers/_search_filters.html.erb` references):
small uppercase tracking-wide secondary label, separator dot, tabular
count. Replaces the prior "Completed · 1" inline label with a more
consistent treatment and adds an "Ongoing · N" header above the active
goals grid.
Name choice: "Ongoing" rather than "Active" because the grid includes
both `active` and `paused` AASM states; "ongoing" reads as still-in-
progress for both. Parallel to the existing "Completed" sibling.
- Sparkline (`savings-sparkline` controller): dropped the `Math.max(0,
yMin)` clamp on the y-axis domain so negative balances (or any series
that dips into negative territory) render fully instead of being
cropped off the canvas.
- Hero card: padding `p-6` → `p-7`, column ratio
`[minmax(0,1fr)_minmax(0,1.6fr)]` so the chart breathes, min height
bumped to 220px, sparkline container `h-full min-h-[200px]` so it
fills the card vertically. Stats row now sits at the bottom of the
text column via `mt-auto pt-6`; labels promoted to `text-xs`, values
to `text-lg`.
- Section vertical rhythm: outer `space-y-6` → `space-y-8`.
- Goal card: padding `p-[18px]` → `p-6`. Internal gap from header row
to amount line `mt-3.5` → `mt-5`. Account-row gap `mt-3` → `mt-4`.
- Account card: padding `p-5` → `p-6`.
- Status pill "Behind" dot: `bg-yellow-500` → `bg-yellow-600` for a
warmer/ambery tone matching the Claude Design reference.
- Goal card donut "behind" stroke: `var(--color-yellow-500)` →
`var(--color-yellow-600)` to match the pill.
- Page header: title "Savings" + "Your savings accounts and the goals
you're working toward." Removed the top-right New goal button (moves
into the Goals section).
- Hero card: "Total in savings" with sum-of-savings-subtype balance,
30-day delta vs last 30 days (Family#savings_balance_30d_delta),
3-stat sub-row (Accounts / Active goals / Saved toward goals), and a
D3 sparkline area chart on the right (new
`savings-sparkline` Stimulus controller, sourced from
Family#savings_balance_series).
- Accounts section: lists Depository accounts with subtype = "savings"
as cards (blue avatar, name, subtype, balance, "Funds N goals"). New
Savings::AccountCardComponent.
- Goals section header: "Goals" + "Save toward what matters." + "New
goal" button right-aligned to the section (not the page header).
- Removed state-filter pill nav. Active goals render in the main grid;
Completed goals get a "Completed · N" divider w/ check-circle icon
and their own grid below.
- Goal card layout reworked: horizontal bar replaced with a 64px donut
ring on the right side of the card header (ring colour tracks
goal.status — yellow=behind, primary=on-track, green=reached). Pill
is inline with the goal name.
- Status pill copy: "Behind pace" → "Behind".
- Filter bar (copied from settings/providers): search input + status
chips (All / On track / Behind / No date). Hidden when ≤ 6 active
goals. Powered by `savings-goals-filter` Stimulus controller —
toggles `.hidden` on cards by goal name + status.
- Family#savings_subtype_accounts, total_savings_balance,
savings_balance_series, savings_balance_30d_delta helpers; controller
computes hero payload + account-goal counts for the cards.
Brings the savings goals UI closer to the Claude Design reference shared
by the user. Changes:
- Sidebar nav label: "Savings goals" → "Savings".
- Status pill copy: "Behind" → "Behind pace" (matches Pill component
from GoalsCommon.jsx).
- Empty state rewritten with a large target icon, "No goals yet"
heading, and the descriptive body copy from the design.
Goal detail page (matches GoalDetail.jsx):
- New "← All goals" back link above the header.
- 2-column hero: ring card on the left (320px column), Projection card
on the right.
- Projection card uses a new D3 Stimulus controller
(`savings-goal-projection-chart`) that draws:
· saved area + line from goal creation → today (solid, primary)
· dashed projection segment from today → target date (yellow when
behind, green when on track)
· horizontal dashed target line with label
· today marker (vertical dashed line + dot)
Data shape comes from `SavingsGoal#projection_payload`.
- Card subtitle generates a contextual sentence ("At $X/mo you'll fall
short. Bump to $Y/mo to hit it on time." / "At your current pace
you'll reach this goal around Month YYYY." / "Goal reached. Nice
work.") with a strong tag highlighting the actionable figure.
- Stat row now shows Linked balance (sum across linked accounts) +
"N accounts" sub-caption instead of duplicate "Target date" stat.
New goal modal (matches the design images 2 + 3):
- DS::Dialog custom header: DS::FilledIcon target glyph + title + step
subtitle ("Step 1 of 2 · Goal details" / "Step 2 of 2 · Review &
start") that updates as the user advances.
- Connected stepper at top of body: numbered circles connected by a
bar, step-1 circle flips to ✓ when complete.
- Step 1 heading "What are you saving for?" + supporting copy.
- Name field paired with a target glyph affordance on its left.
- Target amount + Target date in a 2-col grid.
- Funding accounts list now grouped by account subtype with uppercase
section headers (CHECKING / SAVINGS / HSA / CD / MONEY MARKET /
OTHER), each row showing avatar + name + subtype + balance.
- Step 2 heading "Looks good?" + Review card (goal target + funding
accounts summary + suggested monthly = target/months_remaining), and
a disclosure for the optional initial contribution.
- Footer: "Cancel" left text-button (closes modal) / "Back" left text
when on step 2; "Continue →" or "Create goal →" right arrow button.
Demo generator: Depository accounts now set `subtype` ("checking" /
"savings") on the accountable so they group correctly in the modal.
Tests: all green, 35 runs in the savings suite, 92 assertions.
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).