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

@@ -6,7 +6,7 @@
<%= render Goals::AvatarComponent.new(goal: goal, size: "lg") %>
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2 mb-0.5">
<p class="text-sm font-medium text-primary truncate">
<p class="text-base font-medium text-primary truncate">
<a href="<%= goal_path(goal) %>"
aria-label="<%= aria_label %>"
class="before:absolute before:inset-0 before:rounded-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-alpha-black-100">
@@ -15,7 +15,7 @@
</p>
<%= render Goals::StatusPillComponent.new(goal: goal) %>
</div>
<p class="text-[11px] text-subdued truncate"><%= secondary_line %></p>
<p class="text-xs text-subdued truncate"><%= secondary_line %></p>
</div>
<div class="shrink-0 relative" style="width: <%= Goals::CardComponent::RING_SIZE %>px; height: <%= Goals::CardComponent::RING_SIZE %>px;">
@@ -45,19 +45,19 @@
<div class="mt-5">
<div class="flex items-baseline gap-1.5">
<span class="text-lg font-medium text-primary tabular-nums privacy-sensitive"><%= goal.current_balance_money.format(precision: 0) %></span>
<span class="text-xl font-medium text-primary tabular-nums privacy-sensitive"><%= goal.current_balance_money.format(precision: 0) %></span>
<span class="text-xs text-subdued tabular-nums">/ <%= goal.target_amount_money.format(precision: 0) %></span>
</div>
<% if pace_line %>
<p class="text-[11px] text-subdued tabular-nums mt-1"><%= pace_line %></p>
<p class="text-xs text-subdued tabular-nums mt-1"><%= pace_line %></p>
<% end %>
</div>
<div class="mt-4 flex items-center justify-between">
<div class="flex items-center gap-2">
<%= render Goals::AccountStackComponent.new(accounts: linked_accounts) %>
<span class="text-[11px] text-subdued"><%= linked_accounts_count_label %></span>
<span class="text-xs text-subdued"><%= linked_accounts_count_label %></span>
</div>
<span class="text-[11px] text-subdued tabular-nums"><%= footer_line %></span>
<span class="text-xs text-subdued tabular-nums"><%= footer_line %></span>
</div>
</div>

View File

@@ -30,7 +30,7 @@
<div class="min-w-0">
<p class="text-sm font-medium text-primary truncate"><%= account.name %></p>
<p class="text-xs text-subdued tabular-nums privacy-sensitive">
<p class="text-sm text-subdued tabular-nums privacy-sensitive">
<%= accountable_label(account) %> · <%= row[:balance_money].format(precision: 0) %>
</p>
</div>
@@ -46,11 +46,11 @@
<div class="text-right space-y-0.5">
<div class="flex items-baseline justify-end gap-1.5">
<p class="text-sm font-medium text-primary tabular-nums privacy-sensitive"><%= row[:last_30_money].format(precision: 0) %></p>
<p class="text-[10px] text-subdued"><%= t("goals.show.funding_last_30d") %></p>
<p class="text-xs text-subdued"><%= t("goals.show.funding_last_30d") %></p>
</div>
<div class="flex items-baseline justify-end gap-1.5">
<p class="text-xs text-secondary tabular-nums privacy-sensitive"><%= row[:last_90_money].format(precision: 0) %></p>
<p class="text-[10px] text-subdued"><%= t("goals.show.funding_last_90d") %></p>
<p class="text-xs text-subdued"><%= t("goals.show.funding_last_90d") %></p>
</div>
</div>
</div>

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

View File

@@ -15,8 +15,8 @@
</span>
<details data-color-icon-picker-target="details" data-action="mousedown->color-icon-picker#handleOutsideClick">
<summary class="cursor-pointer absolute -bottom-1 -right-1 flex justify-center items-center bg-surface-inset hover:bg-surface-inset-hover border-2 w-6 h-6 border-subdued rounded-full text-secondary">
<%= icon("pen", size: "xs") %>
<summary class="cursor-pointer absolute -bottom-1 -right-1 flex justify-center items-center bg-surface-inset hover:bg-surface-inset-hover border w-5 h-5 border-subdued rounded-full text-secondary">
<%= icon("pen", size: "xs", class: "w-3 h-3") %>
</summary>
<div class="absolute top-full left-1/2 -translate-x-1/2 mt-2 z-50 bg-container p-3 border border-alpha-black-25 rounded-2xl shadow-xs w-80 max-w-[calc(100vw-2rem)] max-h-[60vh] overflow-y-auto"

View File

@@ -175,9 +175,9 @@
<% end %>
<% end %>
<%# Top row: ring card + projection chart card %>
<%# Top row: ring panel (status, no elevation) + projection chart card %>
<section class="grid grid-cols-1 lg:grid-cols-[320px_minmax(0,1fr)] gap-3">
<div class="bg-container rounded-xl shadow-border-xs p-5 flex flex-col items-center justify-center text-center">
<div class="rounded-xl p-5 flex flex-col items-center justify-center text-center">
<%= render Goals::ProgressRingComponent.new(goal: @goal, size: 180) %>
<p class="text-xl font-medium text-primary tabular-nums privacy-sensitive mt-4"><%= @goal.current_balance_money.format(precision: 0) %></p>
<% unless @goal.completed? %>