You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
There are currently three circular progress-ring implementations in the codebase, each with slightly different chrome, color handling, and accessibility coverage:
Goals::ProgressRingComponent (app/components/goals/progress_ring_component.{rb,html.erb}) — full ViewComponent. Uses the donut-chart Stimulus controller, has role="progressbar" + aria-valuenow + aria-label, sized via size: param, semantic color via percent_text_class → text-success / text-primary. Coupled to a goal: object via the model methods it calls (progress_percent, current_balance_money, target_amount_money, status). Rendered on goals/show.
Goals::CardComponent (app/components/goals/card_component.html.erb:21-43) — inline <svg> with two <circle> elements, rendered in goal index cards at 64px. Uses CSS variables for stroke color, but ring_color previously used palette tokens (just cleaned up to semantic in feat(goals): balance-derived + pledges #1798); the track background still uses var(--budget-unused-fill), which is a budget-namespaced token leaking into goals.
app/views/loans/tabs/_overview.html.erb (incoming via Feat/loans overview new insights #1775) — inline <svg> with raw hex strokes (#3f3f46, #f59e0b), no ARIA, no theme awareness. Currently the worst of the three; should be replaced rather than duplicated.
There's also app/views/budgets/_budget_donut.html.erb and app/views/pages/dashboard/_outflows_donut.html.erb, both using the donut-chart Stimulus controller directly. Those are full segmented donuts (categories), not single-progress rings — they probably stay as-is. But they share the --budget-unused-fill track token.
Proposed shape
A DS::ProgressRing primitive (app/components/DS/progress_ring.{rb,html.erb}) that decouples the visual from any specific model:
DS::ProgressRing.new(percent: 42,# 0..100size: 64,# pxstroke_width: 6,# pxtone: :success,# :success | :warning | :destructive | :neutrallabel: t("..."),# for aria-labelshow_percent: true# render the % in the center)
Then:
Goals::ProgressRingComponent becomes a thin wrapper that resolves goal → tone/percent/label and renders DS::ProgressRing (or gets folded in entirely).
Goals::CardComponent swaps its inline <svg> for DS::ProgressRing.new(percent:, size: 64, tone:).
The loan overview ring (Feat/loans overview new insights #1775) renders DS::ProgressRing.new(percent: loan.balance_paid_ratio * 100, size: 64, tone: ...) instead of inline SVG with raw hex.
Token rename (bundled here)
While extracting, also rename --budget-unused-fill → --color-progress-track-fill (or similar generic name) in design/tokens/sure.tokens.json. Currently the four call sites are split between budgets, goals, and the donut-chart controller — the budget-namespaced name was historical, and a track-fill token serves any donut. Affected:
app/models/budget.rb:210,217 (donut segments — also --budget-unallocated-fill)
budget-unallocated-fill stays under budget: since it's budget-specific.
Why this issue exists
Discovered while reviewing the loan overview PR (#1775). The progress ring there reaches for inline <svg> with raw hex because the existing Goals::ProgressRingComponent is too coupled to a goal: object to reuse. Promoting it to a DS primitive would prevent the next surface (savings sub-accounts, IRA progress, etc.) from rolling its own again.
Out of scope
Segmented donut charts (budgets, dashboard outflows). Those are a different shape; donut-chart Stimulus controller already covers them.
Status semantics — :success/:warning/:neutral is the proposed API but the caller decides how to map their domain status to those tones.
There are currently three circular progress-ring implementations in the codebase, each with slightly different chrome, color handling, and accessibility coverage:
Goals::ProgressRingComponent(app/components/goals/progress_ring_component.{rb,html.erb}) — full ViewComponent. Uses thedonut-chartStimulus controller, hasrole="progressbar"+aria-valuenow+aria-label, sized viasize:param, semantic color viapercent_text_class→text-success/text-primary. Coupled to agoal:object via the model methods it calls (progress_percent,current_balance_money,target_amount_money,status). Rendered ongoals/show.Goals::CardComponent(app/components/goals/card_component.html.erb:21-43) — inline<svg>with two<circle>elements, rendered in goal index cards at 64px. Uses CSS variables for stroke color, butring_colorpreviously used palette tokens (just cleaned up to semantic in feat(goals): balance-derived + pledges #1798); the track background still usesvar(--budget-unused-fill), which is a budget-namespaced token leaking into goals.app/views/loans/tabs/_overview.html.erb(incoming via Feat/loans overview new insights #1775) — inline<svg>with raw hex strokes (#3f3f46,#f59e0b), no ARIA, no theme awareness. Currently the worst of the three; should be replaced rather than duplicated.There's also
app/views/budgets/_budget_donut.html.erbandapp/views/pages/dashboard/_outflows_donut.html.erb, both using thedonut-chartStimulus controller directly. Those are full segmented donuts (categories), not single-progress rings — they probably stay as-is. But they share the--budget-unused-filltrack token.Proposed shape
A
DS::ProgressRingprimitive (app/components/DS/progress_ring.{rb,html.erb}) that decouples the visual from any specific model:Then:
Goals::ProgressRingComponentbecomes a thin wrapper that resolves goal → tone/percent/label and rendersDS::ProgressRing(or gets folded in entirely).Goals::CardComponentswaps its inline<svg>forDS::ProgressRing.new(percent:, size: 64, tone:).DS::ProgressRing.new(percent: loan.balance_paid_ratio * 100, size: 64, tone: ...)instead of inline SVG with raw hex.Token rename (bundled here)
While extracting, also rename
--budget-unused-fill→--color-progress-track-fill(or similar generic name) indesign/tokens/sure.tokens.json. Currently the four call sites are split between budgets, goals, and the donut-chart controller — the budget-namespaced name was historical, and a track-fill token serves any donut. Affected:design/tokens/sure.tokens.json(the definition)app/javascript/controllers/donut_chart_controller.js:194(segment hover fallback)app/models/goal.rb:174,179(donut segments)app/models/budget_category.rb:218,225(donut segments — actually--budget-unallocated-fill, separate)app/models/budget.rb:210,217(donut segments — also--budget-unallocated-fill)budget-unallocated-fillstays underbudget:since it's budget-specific.Why this issue exists
Discovered while reviewing the loan overview PR (#1775). The progress ring there reaches for inline
<svg>with raw hex because the existingGoals::ProgressRingComponentis too coupled to agoal:object to reuse. Promoting it to a DS primitive would prevent the next surface (savings sub-accounts, IRA progress, etc.) from rolling its own again.Out of scope
donut-chartStimulus controller already covers them.:success/:warning/:neutralis the proposed API but the caller decides how to map their domain status to those tones.Refs: #1775 (loan overview), #1798 (goals v2)