Commit Graph

9 Commits

Author SHA1 Message Date
Guillem Arias
25eb18d9af feat(goals): pending-pledge dot on card avatar
Goals with open + unexpired pledges now carry a small amber DS::Pill
dot at the top-right of the avatar on the index card. Same primitive
+ position pattern as the beta gate dot on the sidebar nav, so the
'small marker' affordance reads consistently across the app.

Pledges are preloaded via the existing .includes(:open_pledges, ...) on
the index query, so the indicator is free at request time.
2026-05-18 22:19:44 +02:00
Guillem Arias
82e3ba8ef7 fix(goals): keep archived cards out of the filter loop entirely
The earlier 'filter archived too' attempts kept toggling the archived
section based on chip state, which produced more confusion than value
(filter shows partial counts, archived hides on some chips, etc.).
Step back: archived stays in its own collapsed-by-default section,
always visible, never reacts to the chip / search filter. Render
the cards with filterable: false so they don't add a filter target
in the first place — no JS handling needed, and the active grid +
chips behave exactly like they did before this whole thread.
2026-05-18 22:16:38 +02:00
Guillem Arias
dcb6f391e5 fix(goals): drop hover bg on card, use subtle shadow lift instead
The card was the only place in Sure setting hover:bg-surface-hover on
a bg-container card; every other static card (settings, recurring
transactions, ai_prompts, etc.) stays still. The hover bg (gray-100)
landed almost exactly on top of the ring track (gray-200) and washed
out the pill fill, fighting the very signals the card was trying to
show. Swap to hover:shadow-sm — the lift matches the transaction-tabs
precedent already in Sure, the bg stays white, and ring track + status
pill keep their contrast.
2026-05-18 21:53:09 +02:00
Guillem Arias
33189c2673 ux(goals): polish detail page + unbreak render
- Fix render-blocker: Money#symbol doesn't exist (use #currency.symbol).
- Sanitize projection_summary so the _html locale renders <strong> markup
  instead of escaping it.
- Switch donut + card ring track to --budget-unused-fill;
  --budget-unallocated-fill resolves to the same gray as bg-surface in
  light mode so the unfilled arc was invisible on the detail page.
- Mobile detail: drop avatar, right-align action buttons, stack
  projection header (subtitle + legend) so the subtitle reads on one
  line; bump legend gap on mobile.
- Nowrap the projected reach-date so e.g. "Jul 2026" stays together.
2026-05-15 13:25:03 +02:00
Guillem Arias
f182da79c8 fix(goals): unified per-goal account color map + smaller pen toggle
User flagged two regressions: account colors didn't match between the
goal preview-card avatar stack on the index and the funding-widget
rows on the show page, and the color-picker pen toggle on the new-goal
modal still felt too big.

Color matching:

- `AccountStackComponent` (index card) used
  `Goals::AvatarComponent.color_for(account.name)` — MD5-of-name into
  the 10-color palette.
- `FundingAccountsBreakdownComponent` (show page) recently switched to
  `color_for(account.id.to_s)` — MD5-of-id.
- Same account, two surfaces, two different palette picks. Plus
  either hashing scheme can collide within a multi-account goal
  (palette has 10 colors).

Move ownership to the Goal model: `Goal#account_color_map` returns
`{ account_id => palette_hex }` for the goal's linked accounts. Sort
by `id` for a stable order across reloads, then assign
`palette[i % palette.size]`. Stable + collision-free up to 10
accounts in a single goal (a realistic upper bound — most goals
link 1-3).

Both consumers now read off the same source:

- `AccountStackComponent.new(accounts:, color_map:)` accepts a hash
  and falls back to the name-hash if no map provided (kept for
  callers that don't have a goal in scope yet).
- `FundingAccountsBreakdownComponent#color_for` reads
  `goal.account_color_map[account.id]`.
- Goal card on index passes `goal.account_color_map` to the stack.

Pen toggle:

The new-goal color-picker pen sat in a `w-5 h-5` circle with a
`border` ring + `text-secondary` icon. The border + secondary text
weight kept it loud against the avatar even at 20px. Drop the
border, drop the size another step (`w-4 h-4`), recolor the icon
`text-subdued` + `hover:text-secondary` so the affordance recedes
when not interacted with. Position shifts from `-bottom-1 -right-1`
(8px overhang) to `-bottom-0.5 -right-0.5` (2px overhang) since the
smaller circle doesn't need the larger float. Icon swaps "pen" for
"pencil" (the more conventional edit indicator across Sure).
2026-05-14 22:30:26 +02:00
Guillem Arias
263ccbf5cc 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.
2026-05-14 22:26:53 +02:00
Guillem Arias
a695fb528c fix(goals): align card + banner copy/numbers with show-page changes
Tail of the redundancy + clarity pass:

- Goal card on the index drops ".00" cents from every Money.format
  call (now `format(precision: 0)`). The card uses the same
  visual rhythm as the show page; cents add zero info at this
  scale.
- Card's "behind"-status footer now reads "Save $X/mo to catch
  up" with X = `catch_up_delta_money` (the delta the user must
  add), not the full `monthly_target_amount` (which read as a
  total monthly burn). Same fix as the show-page banner.
- Pending-pledge banner title becomes pluralized: drops "0 days
  left" / "1 days left" grammatical bugs. New locale tree:
    title.zero  → "Pending: $X into Y · expires today"
    title.one   → "Pending: $X into Y · 1 day left"
    title.other → "Pending: $X into Y · N days left"
  Also drops the "Watching for" phrasing (system-talk) for
  "Pending:" (state-talk) and drops cents from the amount.
- `confirm_cancel_body` likewise renders amount without cents.

Cards and banner now read consistently with the show page; one
voice across the surfaces.
2026-05-14 21:19:47 +02:00
Guillem Arias
04422f36b3 a11y(goals/card): scope link accessible name to title + status summary
Whole card was wrapped in <%= link_to ... %>, so screen readers
concatenated every nested text node into one accessible name (~60 words
on a typical card: avatar initial + name + status pill + percent +
balance + target + pace + accounts + footer).

- Outer wrapper now <div> carrying the filter-target + goal-name +
  goal-status data attrs.
- Inner <a> wraps only the goal name. aria-label = "<name>, <status>,
  <percent>% of <target>" — concise SR sentence.
- `before:absolute before:inset-0` makes the inner link's hit area span
  the whole card so sighted users keep the existing click affordance.
- Ring SVG + percent overlay marked aria-hidden (decorative — same info
  already in the aria-label).
- New locale key goals.goal_card.aria_progress.
2026-05-11 20:24:26 +02:00
Guillem Arias
9b61e4a41b refactor: rename Savings Goals feature to Goals
User-facing rename + structural rename. Feature is now called just
"Goals" everywhere — page title, sidebar nav, modal headings, flash
messages, AI assistant tool. Code identifiers follow:

- Models: SavingsGoal → Goal, SavingsContribution → GoalContribution,
  SavingsGoalAccount → GoalAccount.
- Tables: savings_goals → goals, savings_contributions → goal_contributions,
  savings_goal_accounts → goal_accounts. FK columns savings_goal_id →
  goal_id. New migration db/migrate/20260511100003_rename_savings_to_goals.rb
  uses rename_table + rename_column; PG handles index renaming and FK
  redirection automatically.
- Controllers: SavingsGoalsController → GoalsController,
  SavingsContributionsController → GoalContributionsController.
- Routes: /savings_goals → /goals, nested /goals/:id/contributions
  (resource name shifts; old route name aliases dropped).
- ViewComponent namespace: Savings::* → Goals::*. Component class
  names drop their redundant "Goal" prefix where the namespace already
  carries it: Savings::GoalCardComponent → Goals::CardComponent,
  Savings::GoalAvatarComponent → Goals::AvatarComponent. Others keep
  their names (Goals::ProgressRingComponent, Goals::StatusPillComponent,
  Goals::AccountStackComponent, Goals::FundingAccountsBreakdownComponent).
- Stimulus controllers: savings_goal_* → goal_*, savings_goals_filter
  → goals_filter. Stimulus identifiers in data-controller / data-*
  attributes follow.
- Locale keys: savings_goals: → goals: (top level), savings_contributions:
  → goal_contributions: (top level). All t() callers updated.
- AI assistant tool: Assistant::Function::CreateSavingsGoal →
  Assistant::Function::CreateGoal, tool name "create_savings_goal" →
  "create_goal", description / response text updated.
- Sidebar nav label "Savings" → "Goals". Goals/show + index page title
  "Savings" → "Goals". Empty goals_section heading/subtitle dropped
  (duplicated the page title post-rename).

Original migrations create_savings_goals / create_savings_goal_accounts /
create_savings_contributions remain untouched so historical replay
still works; the rename migration runs on top.
2026-05-11 20:08:32 +02:00