V2 rebuilt the pledge create modal but bypassed the DS form helpers
inherited from `StyledFormBuilder`, lost the inline impact preview
from V1's contribution form, and shipped a goal-level "transfer vs
manual_save" toggle that broke on mixed-funding goals.
- Manual `form-field/__body/__label/__input` div-wrapping for the
account select → idiomatic `f.select :account_id, choices,
{ label: t(".account_label") }`. The builder applies the required
marker, error state, and inline-label handling automatically; the
hand-built version drifted from that path and applied
`form-field__input` directly onto the select element, where the
builder picks the correct input class per field type.
- Hand-rolled `<div class="form-error">` + `<p>` loop for errors →
`render "shared/form_errors", model: @pledge` (the shared partial
with the destructive-icon prefix). Matches V1's contribution modal
and the rest of the codebase.
- Drop `class: "btn btn--primary"` on `f.submit` → bare
`f.submit t(".submit")`. The builder's `submit` is wired to
`DS::Button.new(text:, full_width: true)`; the explicit class was
redundant.
- Drop the duplicate "Cancel" button. DS::Dialog already renders an
X in the header; the in-form ghost Cancel was a second close
affordance with no analogue in the new-goal stepper or V1's
contribution form.
- Drop `data: { turbo_frame: "_top" }` on submit. Success already
flows through the controller's `turbo_stream.action(:redirect, …)`
and on 422 the modal frame is the right swap target; the explicit
`_top` was at best redundant and at worst a future Turbo footgun.
- Wire `data-controller="goal-pledge-preview"` on the form and add
an inline preview `<p>` below the amount field. As the user types
the amount, the line updates to "Reaches 75% — $3,750 of $5,000."
or "Hits your $5,000 target — goal reached." Mirrors V1's
contribution preview that V2 dropped on the floor.
- Rename `goal_contribution_preview_controller.js` →
`goal_pledge_preview_controller.js`. Pure rename; the controller
was already domain-neutral.
- Per-account pledge kind. The controller's `default_kind_for(goal)`
picked `transfer` whenever the goal had ANY connected account —
meaning a goal that linked a Plaid checking account AND a manual
cash envelope routed every pledge as `transfer`, including those
the user submitted against the manual account. The reconciler
would then watch for a Transaction that never arrives. Replace
with `kind_for_account(account)` that picks per-account: manual →
`manual_save`, anything else → `transfer`.
- `new` action now respects `?account_id=…` query params and
preselects that account (helpful for the catch-up callout's
inline "Save $X/mo" CTA, which can target a specific account).
Locale: drop the hardcoded "(±5 days, ±$0.50 or ±1%)" tolerance
copy from the helper text — that detail belongs in docs, not in a
modal that fires on every pledge create. Currency-aware copy lands
in commit I. Drop the now-unused `cancel:` key. Add the three
preview templates (`preview_zero`, `preview_nonzero`,
`preview_reached`) consumed by the Stimulus controller.