fix(goals): scale up card/widget/chart text, fix chart continuity, ease ring focal point

Five small audit follow-ups bundled because they were each one-line
swaps and individually wouldn't earn their own commit.

Card text scale (vs Sure house style — budget_category h3 ≈ text-base,
budget _actuals_summary value text-xl, account row text-sm subtype):
- goal card title text-sm → text-base
- goal card balance text-lg → text-xl
- goal card pace/footer/subtitle text-[11px] → text-xs
- funding row subtype subtitle text-xs → text-sm
- funding row "last 30d / last 90d" labels text-[10px] → text-xs

Chart label scale (projection chart was an outlier at font-size: 10
while time_series_chart_controller uses 12):
- every `font-size: 10` in goal_projection_chart_controller.js → 12
- tooltip cssText font-size: 11 → 12

Color-picker pen toggle on the new-goal avatar was w-6 h-6 (24px
circle, ~55% of the lg 44px avatar). Shrink to w-5 h-5 + add a w-3 h-3
class on the inner icon so it scales down with it.

Graph continuity bug: the saved-line endpoint and the projection-line
start point could disagree by tens of $thousands. Saved came from
`Balance::ChartSeriesBuilder` (daily snapshot in `balances`),
projection started at `currentAmount = goal.current_balance.to_f`
(live `linked_accounts.sum(:balance)`). When the snapshot lagged
the live read, the chart showed a vertical gap at the "today" marker.

Filter any same-day-or-later points out of the raw saved series,
always extend the saved series to `(today, currentAmount)`. Saved
line now closes at exactly the projection's start. The recent
balance-drop story is still honestly shown (the line dips toward
the live value rather than ending at the stale snapshot).

Ring card focal-point (RUI audit): the left ring card on goals#show
sat at the same `shadow-border-xs` elevation as the projection chart
and funding card. "When every card is raised, nothing's primary."
Drop the shadow + container background — the ring now reads as a
status panel sitting on the page surface, not a content card
competing with its neighbours. Paused/archived/celebration/empty
right-slot variants keep elevation since they ARE content cards.

Deferred: light-mode pink distribution-bar contrast. The fix needs
a DS token decision (hairline outline vs darker step on the palette
entries); rolling it into a polish PR risks dragging in DS changes
unrelated to goals. Logged for a follow-up.
This commit is contained in:
Guillem Arias
2026-05-14 22:26:41 +02:00
parent ef94b913c1
commit 263ccbf5cc
5 changed files with 33 additions and 25 deletions

View File

@@ -89,7 +89,14 @@ export default class extends Controller {
const endDate = target || new Date(today.getTime() + 30 * 24 * 60 * 60 * 1000);
const rawSavedSeries = (data.saved_series || []).map((p) => ({ date: new Date(p.date), value: p.value }));
// Drop any same-day-or-later points from the balance series: we own the
// endpoint with `currentAmount` (live `linked_accounts.sum(:balance)`)
// so the saved line meets the projection's starting point with no gap.
// Without this, the snapshot in `balances` for today could differ from
// the live read (sync timing) and the chart showed a vertical jump.
const rawSavedSeries = (data.saved_series || [])
.map((p) => ({ date: new Date(p.date), value: p.value }))
.filter((p) => p.date < today);
const firstContribDate = rawSavedSeries[0]?.date;
const savedSeries = [];
// Only seed a (start, 0) point when start_date predates the first
@@ -99,9 +106,10 @@ export default class extends Controller {
savedSeries.push({ date: start, value: 0 });
}
savedSeries.push(...rawSavedSeries);
if (savedSeries.length && savedSeries[savedSeries.length - 1].date < today) {
savedSeries.push({ date: today, value: currentAmount });
}
// Always close the saved line at (today, currentAmount) — the projection
// line starts here too, guaranteeing visual continuity at the today
// marker.
savedSeries.push({ date: today, value: currentAmount });
const projectionEnd = target
? Math.max(currentAmount, currentAmount + avgMonthly * Math.max(0, this._monthsBetween(today, target)))
@@ -179,7 +187,7 @@ export default class extends Controller {
.attr("x", margin.left - 6)
.attr("y", y(tickValue) + 3)
.attr("text-anchor", "end")
.attr("font-size", 10)
.attr("font-size", 12)
.attr("fill", textSecondary)
.text(this._fmtMoneyShort(tickValue, data.currency));
});
@@ -205,7 +213,7 @@ export default class extends Controller {
.attr("x", margin.left - 6)
.attr("y", targetY + 3)
.attr("text-anchor", "end")
.attr("font-size", 10)
.attr("font-size", 12)
.attr("fill", textPrimary)
.text(`Target · ${this._fmtMoneyShort(targetAmount, data.currency)}`);
} else {
@@ -215,7 +223,7 @@ export default class extends Controller {
.attr("x", margin.left + innerWidth - 4)
.attr("y", targetY - 6)
.attr("text-anchor", "end")
.attr("font-size", 10)
.attr("font-size", 12)
.attr("fill", textPrimary)
.text(`Target · ${this._fmtMoney(targetAmount, data.currency)}`);
}
@@ -308,7 +316,7 @@ export default class extends Controller {
.attr("x", x(target) - 8)
.attr("y", y(projectionEnd) - 8)
.attr("text-anchor", "end")
.attr("font-size", 10)
.attr("font-size", 12)
.attr("fill", textSecondary)
.text(labelText);
}
@@ -344,7 +352,7 @@ export default class extends Controller {
.append("text")
.attr("x", x(today) + 10)
.attr("y", y(pendingTop) + 4)
.attr("font-size", 10)
.attr("font-size", 12)
.attr("fill", textSecondary)
.text(`+ pending ${this._fmtMoneyShort(pendingPledgeAmount, data.currency)}`);
}
@@ -375,7 +383,7 @@ export default class extends Controller {
.attr("x", x(today))
.attr("y", margin.top - 4)
.attr("text-anchor", "middle")
.attr("font-size", 10)
.attr("font-size", 12)
.attr("fill", textSecondary)
.text("Today");
}
@@ -395,7 +403,7 @@ export default class extends Controller {
.attr("x", (d) => x(d))
.attr("y", height - 8)
.attr("text-anchor", "middle")
.attr("font-size", 10)
.attr("font-size", 12)
.attr("fill", textSecondary)
.text((d) => tickFmt(d));
// De-dupe adjacent equal tick labels (e.g. multiple "May '26" on a
@@ -439,7 +447,7 @@ export default class extends Controller {
if (root.style.position !== "absolute") root.style.position = "relative";
const tooltip = document.createElement("div");
tooltip.style.cssText = "position:absolute;pointer-events:none;display:none;background:var(--color-gray-900);color:var(--color-white);font-size:11px;line-height:1.35;padding:6px 8px;border-radius:6px;white-space:nowrap;z-index:5;box-shadow:0 2px 8px rgba(0,0,0,0.15);";
tooltip.style.cssText = "position:absolute;pointer-events:none;display:none;background:var(--color-gray-900);color:var(--color-white);font-size:12px;line-height:1.35;padding:6px 8px;border-radius:6px;white-space:nowrap;z-index:5;box-shadow:0 2px 8px rgba(0,0,0,0.15);";
root.appendChild(tooltip);
const overlay = svg