feat(savings_goals/new): live previewable name avatar + ghost cancel + circular header icon

Replace the big square DS::FilledIcon next to the name input with a
small Savings::GoalAvatarComponent that previews the goal's avatar
(seeded color + first character of the typed name, updates live via
new stepper#nameChanged action).

Switch the modal header's target avatar from FilledIcon(size: lg,
rounded: false) → (size: md, rounded: true) — matches the goal-avatar
shape used elsewhere on the page.

Replace the hand-rolled <button> for Cancel/Back with DS::Button
variant: "ghost". Stepper now drills into the button's inner span to
swap the label, same pattern already used for the Continue/Create
button on the right.

Drop the now-unused footerLeftLabel target.
This commit is contained in:
Guillem Arias
2026-05-11 14:59:26 +02:00
parent c04b306bfd
commit e3dd1c4c1e
3 changed files with 29 additions and 15 deletions

View File

@@ -15,6 +15,9 @@ export default class extends Controller {
"step2Circle",
"stepperLine",
"modalSubtitle",
"nameInput",
"amountInput",
"avatarPreview",
"linkedAccountCheckbox",
"initialContributionAmount",
"initialContributionAccountSelect",
@@ -23,7 +26,6 @@ export default class extends Controller {
"reviewAccounts",
"reviewSuggested",
"footerLeftButton",
"footerLeftLabel",
"footerRightButton",
"submitButton",
];
@@ -91,6 +93,14 @@ export default class extends Controller {
this.updateReview();
}
nameChanged() {
if (!this.hasAvatarPreviewTarget || !this.hasNameInputTarget) return;
const name = this.nameInputTarget.value.trim();
const initial = name ? name.charAt(0).toUpperCase() : "?";
const inner = this.avatarPreviewTarget.querySelector('[data-testid="savings-goal-avatar"]');
if (inner) inner.textContent = initial;
}
validateStep1() {
const requiredInputs = this.step1PanelTarget.querySelectorAll(
'input[name="savings_goal[name]"], input[name="savings_goal[target_amount]"]'
@@ -163,9 +173,12 @@ export default class extends Controller {
}
updateFooter() {
if (this.hasFooterLeftLabelTarget) {
this.footerLeftLabelTarget.textContent =
this.currentStep === 1 ? this.cancelLabelValue : this.backLabelValue;
if (this.hasFooterLeftButtonTarget) {
const labelSpan = this.footerLeftButtonTarget.querySelector("span");
if (labelSpan) {
labelSpan.textContent =
this.currentStep === 1 ? this.cancelLabelValue : this.backLabelValue;
}
}
if (this.hasFooterRightButtonTarget) {
const labelSpan = this.footerRightButtonTarget.querySelector("span");