Files
sure/savings-audit-refactoring-ui.md
Guillem Arias cf4e560a4c feat(goals): extract shared color_icon_picker controller; add icon to goals; tinted avatar
User requested replacing the in-house color disclosure with the
categories color+icon popover. Done as a controller extraction so
categories and goals share one Stimulus controller (user's option:
"Extract a shared color_icon_picker_controller.js").

- `git mv` app/javascript/controllers/category_controller.js to
  color_icon_picker_controller.js. Categories form + color_avatar
  partial updated to use the new identifier (data-controller=
  "color-icon-picker", target/action selectors renamed).
- Goal model gains an icon column (migration
  20260511190000_add_icon_to_goals.rb) + ICONS = Category.icon_codes
  + inclusion validation. GoalsController permits :icon in
  goal_params + goal_update_params.
- Goals::AvatarComponent now renders icon when present (falls back to
  first-letter initial), and adopts the Categories tinted-bg + colored
  -content style (bg = `color-mix(in oklab, COLOR 10%, transparent)`,
  text/icon = COLOR). Matches the picker's live preview so what the
  user sees during selection equals the saved state.
- New goals/_color_picker.html.erb mirrors categories/_form's popover:
  avatar + pen overlay summary + popup with color row (+ rainbow
  custom-hex trigger) + icon grid. Pickr / contrast validation / auto-
  adjust all inherited from the shared controller.
- Stepper step 1 layout: drop the inline letter-avatar (data-goal-
  stepper-target="avatarPreview") in favour of the picker avatar next
  to the name input. Step 1's tail no longer renders a separate color
  partial. Edit form passes icons local through.

Verified live: new goal modal renders 11 color radios (10 presets +
custom) + 141 icon radios + pen-summary; categories form still
operational (no console errors) under the renamed controller.
2026-05-11 21:28:23 +02:00

21 KiB
Raw Blame History

Savings Goals — Refactoring UI Audit

Branch feat/savings-goals (we-promise/sure). Read-only audit using Wathan & Schoger's Refactoring UI. Each finding cites a file:line, severity (P0 ship-blocker, P1 fix-before-merge, P2 nice-to-have), the RUI rule it breaks, and a minimal fix. Grouped by surface; Top-10 "ship now" at the bottom.

Coverage caveat

Shots in audit-shots/. Clean captures: index (light/dark/mobile), behind/on-track/no-target-date/reached/paused show, new-goal modal step 1, mobile show. Demo data regenerated mid-session and invalidated some goal IDs, so edit-modal, add-contribution modal, contribution delete-confirm, archived show, no-accounts empty state, and some mobile modal states were read from source rather than from screenshots. All findings are still grounded in real code paths.


1. Index — populated (light/dark/mobile)

app/views/savings_goals/index.html.erb:11-68P1 — KPI strip gives three metrics identical visual weight. Hierarchy — actions/stats live in a pyramid. All three cards use text-3xl font-medium on bg-container; nothing answers the user's actual landing question ("am I winning?"). Elevate "Goals on track" to a primary card with a larger numeral + a ring/bar; demote velocity + needs to compact secondary cards (text-xl, tighter padding).

app/views/savings_goals/index.html.erb:13,36,49P2 — Three card panels read as one grey block in dark mode. Depth — light-from-above. bg-container differs from bg-app by only ~4% L* in dark. Add a faint top-edge highlight (ring-1 ring-inset ring-white/5) so cards read as raised, not tinted.

app/views/savings_goals/index.html.erb:14,37,50P2 — Three adjacent eyebrows text-[11px] uppercase tracking-wide text-secondary create striped chrome. Typography — all-caps as decoration. Commit to one all-caps style (KPI eyebrows OR section headings, not both).

app/views/savings_goals/index.html.erb:92-119P1 — Search input and chip group compete. Forms — most-common action wins the row. Search uses border border-secondary bg-container; chips use bg-surface-inset segmented. Drop the search border + bg-surface-inset, cap to md:max-w-xs. At 1440 search currently stretches 600+px and swallows the chip group's importance.

app/views/savings_goals/index.html.erb:107P1 — Chip filter values duplicate status-pill semantics but in a different visual language. Color — single palette across the app. Add a 1.5×1.5 colored dot prefix to each chip (same green/yellow/grey as the pill) so filter and chip share visual identity.

app/views/savings_goals/index.html.erb:123-127P2 — "ONGOING · 5" section heading shares chrome with KPI eyebrows. Hierarchy — don't repeat your hierarchy gestures. Use text-sm font-medium text-secondary for section headings; reserve all-caps eyebrows for KPI cards.

app/components/savings/goal_card_component.html.erb:8-41P1 — Card has three competing focal points: avatar + name + pill, big balance/target, and a ring with overlaid percent. Cards — one job per card. Percent inside the ring repeats current/target underneath. Drop one; let geometry tell the story OR let the numbers.

app/components/savings/goal_card_component.html.erb:33-35P2stroke-linecap="round" is on the progress arc only. Finishing touches — consistency. Apply to both circles for future-proofing partial-track variants.

app/components/savings/goal_card_component.html.erb:46P2/ $50,000.00 is text-xs text-subdued. Typography — hierarchy via weight not just color. In dark mode at 12px the slash + number sit near noise. Bump to text-secondary; the slash already marks this as secondary.

app/components/savings/goal_card_component.html.erb:53-58P2 — Footer line wraps on narrow cards. Spacing — fixed widths break. Stack vertically (flex-col gap-1) or shorten to "+$1,531/mo to catch up" (cents are noise at card density).

app/components/savings/account_stack_component.html.erb:3-12P2 — 20px avatars with text-[9px] initials are unreadable. Imagery — intended sizes. Bump to 24px or drop initials and rely on hover-title.

app/components/savings/account_stack_component.html.erb:3P1ring-2 ring-container collapses in dark mode (ring color matches page bg). Depth — rings fake separation from the surface beneath. Use ring-app when the stack is on the page surface, ring-container when on a card.


2. Show — header & action region

app/views/savings_goals/show.html.erb:2-7P1 — H1 + status pill share a row; pill is text-xs next to text-2xl. Hierarchy — status is meta, not a peer of the name. Move pill to the secondary line. Long names ("Investment property downpayment") currently truncate to "House …" on mobile because of the pill.

app/views/savings_goals/show.html.erb:33-49P1 — Edit (outline)

  • Add contribution (primary) + kebab. Hierarchy — action pyramid. Pause/Resume/Complete/Archive are state changes hidden in the kebab after the primary CTA. Promote Pause/Resume to a ghost button beside Edit; keep Archive/Delete in the menu.

app/views/savings_goals/show.html.erb:6-22P2 — Subtitle joins target amount + date + days-left with " · ". Typography — line length. At 1440 ~80ch in one parse-heavy sentence. Stack: deck line under H1, then meta on next line.

app/views/savings_goals/show.html.erb:26-31P2 — "Last contribution 30 days ago" uses mt-0.5 — ambiguous grouping with the subtitle. Spacing. Increase to mt-2 and give it a clock-3 icon.


3. Show — alert banners

app/views/savings_goals/show.html.erb:81-127P1 — Three mutually-exclusive banners use the wrong variants. Color — variant maps to intent. Paused = info (blue), archived = info (blue), catch-up = warning (yellow, correct). Paused is a user-chosen neutral state, not info; archived is historical. Use a neutral banner (bg-surface-inset) for paused + archived.

app/views/savings_goals/show.html.erb:86-89,98-101P0 — Resume/Restore CTAs use raw class="inline-flex items-center gap-1 rounded-md px-3 py-2 ... bg-inverse hover:bg-inverse-hover". Finishing touches — supercharge defaults. Re-implements the primary button by hand — focus ring, loading state, disabled state diverge. Use DS::Button / DS::Link like the catch-up CTA at line 117-124 already does.

app/views/savings_goals/show.html.erb:108P2 — Catch-up title "Save $1,531.25/mo to catch up" repeats verbatim in the CTA "Add $1,531.25". Hierarchy — redundant verbs. Title states the rate; CTA should state the verb ("Add this month" or "Add contribution").


4. Show — ring + projection

app/views/savings_goals/show.html.erb:130-140P1 — Ring card shows percent in donut center AND $1,320 of $2,400 · $1,080 to go underneath. Cards — focal point. Same redundancy as goal card but louder. Strip the percent from the ring or strip the dollar line.

app/views/savings_goals/show.html.erb:185P2 — Projection arc color picks green vs yellow from status. Color — limited palette is a feature. For paused goals the projection still draws a confident forecast (see Sabbatical screenshot). When paused, color the projection var(--color-gray-400) and label it "If you resume."

app/views/savings_goals/show.html.erb:179-201P1 — Chart card stacks heading + summary + legend above min-h-[200px] chart. Spacing — charts need room. At 1280 the summary wraps to two lines and eats chart height. Move the summary into the chart as an annotation, or push it below as a caption.

app/views/savings_goals/show.html.erb:142-158P2 — Reached celebration card = 64px disc icon + heading + body + archive button. Finishing touches — celebration moments deserve reward. Add a subtle pattern or a mini saved-progress chart so the "$15k done in 18 months" story lands.

app/views/savings_goals/show.html.erb:159-177P2 — No-target- date card uses identical chrome to the celebration card (h3 + p + sm outline button). Hierarchy — different intents should look different. Use bg-green-500/10 accent for celebration only; keep no-target neutral with smaller body copy.


5. Show — stats row + bottom row

app/views/savings_goals/show.html.erb:209-229P1 — Combo pace card crams 5 facts on two lines: avg + /mo + target + delta. Typography — chunking. The text-2xl + text-sm + text-subdued baseline-mix forces left-to-right prose reading. Split into two side-by-side stats (Avg vs Target) OR put the "Behind by" delta into a text-warning pill on row 1 — current text-subdued hides the whole point.

app/views/savings_goals/show.html.erb:233-237P1 — Total contributions card displays "12 · Across all accounts" — not linked, not actionable. Cards — make stats actionable. Link to scroll/filter the list below or replace with a more useful stat (median amount, biggest this month). The "Across all accounts" label is also wrong for single-account goals.

app/views/savings_goals/show.html.erb:241-256P2 — Two-column [1.6fr | 1fr] clips both columns at 1280. Spacing — relative weight should match density. Equal columns or stack at lg below 1280.

app/views/savings_goals/_contributions_list.html.erb:10-44P2 — Row px-2 py-2 is tight. Spacing — list rows want breathing. Bump to py-3.

app/components/savings/funding_accounts_breakdown_component.html.erb:4-10P1 — Stacked bar is h-2. Dashboards — data viz needs minimum size. 8px is below the threshold where color differences register — especially dark mode. Bump to h-3 with ring-inset ring-black/5.

app/components/savings/funding_accounts_breakdown_component.html.erb:18P2 — Meta line text-[11px] and percent text-[10px] are off-scale. Typography — type ramp. The design system jumps 12→14→16. Use text-xs text-subdued consistently.

app/components/savings/funding_accounts_breakdown_component.html.erb:7P2 — Bar segment uses MD5(name) color. Color — deterministic identity is good, hierarchy is bad. If two accounts hash close, segments blur. Post-process to shift adjacent segments through the palette.


6. New-goal modal — step 1

app/views/savings_goals/_form_stepper.html.erb:9-19P1 — Stepper labels are equal-weight, only the fill differentiates. Forms — progress disclosure. Make active circle 32px and inactive 28px so focus reads through size, not just color.

app/views/savings_goals/_form_stepper.html.erb:30-32P2 — Avatar preview at size: "md" (36px) vs xl (64px) on the show page. Forms — visual feedback should match destination. Use size: "lg" (44px).

app/views/savings_goals/_form_stepper.html.erb:43-53P1 — Target amount + target date in grid-cols-2. Money field uses styled form chrome; date field uses native HTML date input. Forms — side-by- side requires same input language. Match chrome on the date field or stack them.

app/views/savings_goals/_form_stepper.html.erb:56-87P0 — Funding accounts list has no helper text. Forms — required fields visible. Empty submit shows a tiny error below the list. Add a hint under the section label: "Choose where contributions will come from."

app/views/savings_goals/_form_stepper.html.erb:64-74P1 — Checkbox + row click target is good but checked state is only a 16×16 checkmark. Selectable cards. Checked row should swap to bg-surface-inset with a filled-blue checkbox; hover stays subtle.

app/views/savings_goals/_form_stepper.html.erb:80P2 — Balance column matches account name weight (text-sm font-medium). Typography. Bump balance to text-secondary so the eye distinguishes selectable label from metadata.

app/views/savings_goals/_form_stepper.html.erb:89-94P2 — Notes disclosure is right-aligned; breaks scanning. Forms — progressive disclosure. Left-align like the rest of the form.

app/views/savings_goals/_form_stepper.html.erb:96P2 — Color field is hidden in step 1; only edit form exposes the palette. Forms — silent state. Either expose a small swatch row by the name field or document the auto-pick.


7. New-goal modal — step 2

app/views/savings_goals/_form_stepper.html.erb:99-123P2 — Review card weights "Funding accounts: 2" and "Suggested monthly: $X/mo" equally. Hierarchy — review should restate the commitment. Suggested monthly is the actionable fact; weight it as text-base text-primary.

app/views/savings_goals/_form_stepper.html.erb:125-152P1 — Initial-contribution disclosure has include_blank: "Select account" on the select. If user opens it and forgets the select, submit silently fails or zero-submits. Forms — completeness. Either require the account when disclosure is open or auto-populate with the first linked account.

app/views/savings_goals/_form_stepper.html.erb:155-181P2 — Footer uses hidden (not invisible) on the Back button. Forms — nav visibility. Continue button slides between steps. Use invisible or ml-auto on Continue.


8. Edit modal

app/views/savings_goals/_form_edit.html.erb:23-34P1 — Color palette = 6 24×24 swatches with peer-checked:ring-2. Forms — selectable swatches; imagery — tap targets. 24px is below iOS 44px threshold. Bump to 32px, add aria-label per radio, show a check icon inside selected swatch.

app/views/savings_goals/_form_edit.html.erb:38P2 — Notes textarea is 2 rows; stepper form's notes is 3 rows. Forms — match textarea sizing. Use 3.

app/views/savings_goals/_form_edit.html.erb:40-42P2 — Bare f.submit without explicit variant. Buttons — supercharge defaults. Wrap in DS::Button like new modal does.

app/views/savings_goals/edit.html.erb:1-7P2 — Edit uses default DS::Dialog title; new uses custom header with FilledIcon. Consistency — same logical action, different modal frame. Match headers or downgrade new.


9. Add-contribution modal

app/views/savings_contributions/new.html.erb:11-23P2 — Form order is fine but the account select has include_blank even when only one account is linked. Finishing touches — smart defaults. Pre- select first account when there's only one.

app/views/savings_contributions/new.html.erb:14P2 — Money field uses hide_currency: true. Forms — currency clarity. If the goal's currency differs from primary, the user can mis-type. Show a currency badge or put it in the label.

app/views/savings_contributions/new.html.erb:25-27P2 — Same bare f.submit as edit modal. Wrap in DS::Button.


10. Contribution row — kebab + delete-confirm

app/views/savings_goals/_contributions_list.html.erb:24-43P1 — Kebab only renders for contribution.manual?. Non-manual rows show an invisible w-9 h-9 placeholder. Spacing — don't reserve space silently. Good for alignment but no affordance for "why no kebab." Add a small lock icon or "Imported" tag in the source line.

app/views/savings_goals/_contributions_list.html.erb:33-38P1CustomConfirm for delete uses destructive: true. Modals — destructive needs clear out. Confirm cancel button text is "Cancel" or "Keep," not modal-chrome "Close" — RUI calls out action-named cancel buttons for destructive confirms.


11. Status pill — 5 variants

app/components/savings/status_pill_component.rb:3-8P1 — Two variants share bg-green-500/10 text-success (on_track + reached); two share bg-surface-inset text-secondary (no_target_date + paused). Color — each meaningful state needs distinct visuals. Reached and on-track are semantically different. Same for no-date and paused. Give reached an amber/gold accent; give paused text-subdued to mute it.

app/components/savings/status_pill_component.html.erb:1-4P2 — Pill gap-1 is tight at text-xs. Imagery — pill density. Bump to gap-1.5 and tracking-tight.

app/components/savings/status_pill_component.rb:6P2 — Icon for no_target_date is infinity. Reads as "unlimited" not "no deadline." Use calendar-x or calendar-question.


12. Funding accounts breakdown

app/components/savings/funding_accounts_breakdown_component.html.erb:1-2P2 — Empty state is one <p>. Empty states — don't leave users hanging. Add a muted icon + CTA "Add your first contribution."

app/components/savings/funding_accounts_breakdown_component.html.erb:12-26P2space-y-3 between 3-line rows visually merges them. Spacing — list density. Use divide-y divide-subdued or space-y-4.


13. Empty state — first run

app/views/savings_goals/_empty_state.html.erb:3-29P1 — Icon + heading + body + button is functional but visually generic. Empty states — first-run sells the feature. Replace the 32px target icon with a muted hero illustration showing what a populated goal looks like.

app/views/savings_goals/_empty_state.html.erb:19-26P0 — When linkable_account_count == 0, CTA goes to new_account_path with no return path. Empty states — guide the flow. After account creation the user lands on /accounts/new redirects, not /savings_goals. Add ?return_to=/savings_goals and a 2-step preview ("1. Connect 2. Set").

app/views/savings_goals/_empty_state.html.erb:5-7P2 — Icon container bg-surface-inset differs from bg-container by ~5% L*. Depth. Use bg-app to invert the relief (card > inset > icon).


14. Mobile (375×667)

app/views/savings_goals/index.html.erb:11P1 — KPI strip collapses to single-column. Dashboards — mobile collapse. Three stacked full-width cards read as a notifications page. 2x2 with one spanning, or compact "stat lines" (eyebrow + numeral inline).

app/views/savings_goals/show.html.erb:33-78P0 — Header action group truncates the goal name on mobile (captured: "House …"). Hierarchy — action bar must collapse. Demote Edit + kebab to a sheet; keep only "Add contribution" visible. Name must always show.

app/views/savings_goals/show.html.erb:130P1 — Stacked ring/chart cards on mobile have no gap. Spacing. Add space-y-3 on the section so eye doesn't flow from "13%" into the chart axis.

app/views/savings_goals/_form_stepper.html.erb:155-176P2 — Continue button isn't sticky on mobile. Forms — mobile primary CTA. After selecting accounts, user scrolls back up to find Continue. Make the footer sticky bottom-0 on mobile.


15. Sidebar, breadcrumbs, header chrome

app/views/savings_goals/index.html.erb:2-4P2 — Subtitle "Your savings accounts and the goals you're working toward" shows every visit. Typography — page subtitles carry decoration not info. Replace with current-period context ("$2,940 saved in May 2026") or hide after first visit.

config/locales/breadcrumbs/en.yml (savings entry) — P2 — "Home Savings Goal name" on mobile wastes ~40px vertical. Nav — levels. Drop "Home" on mobile or replace with a back chevron.

app/views/savings_goals/show.html.erb:2P2 — No explicit "Back to Savings" link near the H1. Nav — back affordance. The breadcrumb is chrome, not content. Add an arrow-left button next to the avatar.


Top 10 ship-now

  1. show.html.erb:86-89,98-101 P0 — Resume/Restore banner CTAs reimplement the primary button by hand. Replace with DS::Button so focus/hover/disabled match.
  2. show.html.erb:33-78 mobile P0 — Header truncates goal name on mobile. Demote Edit + kebab to a sheet, keep only Add contribution.
  3. _form_stepper.html.erb:56-87 P0 — Funding accounts list needs an explicit hint before the user clicks Continue.
  4. _empty_state.html.erb:19-26 P0 — No-accounts state needs a return-to-savings_goals path after account creation + 2-step preview.
  5. index.html.erb:11-68 P1 — Three equal-weight KPIs hide the one answering "am I winning?". Elevate "Goals on track" to primary card.
  6. status_pill_component.rb:3-8 P1 — Reached + on-track share green; paused + no-target share grey. Give reached a gold accent; give paused a true muted look.
  7. show.html.erb:130-140 + goal_card_component.html.erb:8-41 P1 — Ring + numeric percent + dollar/target trio is redundant. Drop one.
  8. show.html.erb:81-127 P1 — Paused + archived banners use info-blue. Use neutral; reserve info-blue for actual info.
  9. index.html.erb:92-119 P1 — Search/chip toolbar mismatch. Cap search at max-w-xs, drop its border, add colored dots to chips.
  10. funding_accounts_breakdown_component.html.erb:4 P1 — Stacked bar h-2 is too thin. h-3 + 1px inset ring lifts it from decoration to data.

Closing notes

  • Screenshots in /Users/guillem.arias/Documents/gariasf/sure/audit-shots/.
  • No code edits made. Browser closed.
  • Surfaces read from source rather than captured: add-contribution modal, contribution delete-confirm, archived show, no-accounts empty state, most mobile modal states. Findings still ground in real code.