ux(goals): redesign show page — one CTA, calm banners

Header collapses to title + kebab. The status pill and the `Record pledge`
button leave the title row. Status moves into a one-line callout below the
subtitle that doubles as the catch-up demand when behind, the
reach-date when on track, or a prompt for a target date when missing.

`Record pledge` is now the only pledge entry point on the page and lives
under the ring. Behind goals pre-fill it with the catch-up delta.

The standalone catch-up alert card is gone — its title is the callout, its
pace breakdown moves into the projection chart's subtitle, and its CTA
is the ring-adjacent button. The "Adjust target instead" link is
absorbed into the kebab's existing Edit item.

Pending-pledge banner switches from a warning Alert to a neutral
container chip. It is informational state, not a warning. Title carries
the relative pledged-at meta inline; verbose auto-confirms body stays
but in subdued size.

Projection chart drops the today-line pending stub (vertical line +
dashed marker + "+ pending $X" text). That data already lives in the
pending banner above the chart; the duplicate annotation clutters the
today line, the small dashed circle reads as misaligned at small pending
amounts, and the label overlaps the projection trajectory. Shortfall
label gets a paint-order halo so it stays legible across the dashed
projection line.
This commit is contained in:
Guillem Arias
2026-05-15 14:11:23 +02:00
parent 33189c2673
commit 314113e582
6 changed files with 128 additions and 116 deletions

View File

@@ -146,8 +146,6 @@ export default class extends Controller {
]
: [];
const pendingPledgeAmount = data.pending_pledge_amount || 0;
const yMax = Math.max(targetAmount * 1.05, projectionEnd, requiredEnd, currentAmount, 1);
const x = d3.scaleTime().domain([start, endDate]).range([margin.left, margin.left + innerWidth]);
@@ -332,47 +330,15 @@ export default class extends Controller {
.attr("text-anchor", "end")
.attr("font-size", 12)
.attr("fill", textSecondary)
.attr("paint-order", "stroke")
.attr("stroke", containerBg)
.attr("stroke-width", 4)
.attr("stroke-linejoin", "round")
.text(labelText);
}
}
}
if (pendingPledgeAmount > 0 && target) {
const willHit = projectionEnd >= targetAmount;
const pendingColor = willHit ? "var(--color-green-600)" : "var(--color-yellow-600)";
const pendingTop = Math.min(yMax, currentAmount + pendingPledgeAmount);
svg
.append("line")
.attr("x1", x(today))
.attr("x2", x(today))
.attr("y1", y(currentAmount))
.attr("y2", y(pendingTop))
.attr("stroke", pendingColor)
.attr("stroke-width", 3)
.attr("stroke-linecap", "round")
.attr("opacity", 0.4);
svg
.append("circle")
.attr("cx", x(today))
.attr("cy", y(pendingTop))
.attr("r", 5)
.attr("fill", containerBg)
.attr("stroke", pendingColor)
.attr("stroke-width", 2)
.attr("stroke-dasharray", "2 2");
if (innerWidth >= 320) {
svg
.append("text")
.attr("x", x(today) + 10)
.attr("y", y(pendingTop) + 4)
.attr("font-size", 12)
.attr("fill", textSecondary)
.text(`+ pending ${data.pending_pledge_label_short}`);
}
}
svg
.append("line")
.attr("x1", x(today))