fix(ds): unify tab/chip controls on DS::SegmentedControl (#8) (#2284)

* fix(ds): goals status filter -> DS::SegmentedControl

The goals status chips were a hand-rolled segmented control: active state
toggled via ad-hoc bg-container/shadow-border-xs/text-* classes, NO hover on
inactive chips, and a light-only focus ring (ring-alpha-black-100, invisible in
dark mode). Migrate to DS::SegmentedControl button segments + toggle the
canonical --active class in goals_filter_controller#syncChipState. Gains the
dark-safe hover (hover:bg-gray-200 / theme-dark:hover:bg-gray-800) and the
canonical focus ring. First control in the tab-consistency pass (#8).

* fix(ds): provider filter chips -> DS::SegmentedControl

Same hand-rolled segmented control as the goals chips: active toggled via
ad-hoc bg-container/shadow/text classes, no inactive hover, light-only
ring-alpha-black-100 focus ring. Migrate to DS::SegmentedControl + toggle the
canonical --active class in providers_filter_controller#syncChipState.

* fix(ds): transaction-type tabs -> DS::SegmentedControl

Expense/Income/Transfer tabs were hand-rolled with a hover==active affordance
bug: inactive hover raised bg to bg-container, identical to the active state, so
hover and selected were indistinguishable. Migrate to DS::SegmentedControl (link
segments + icons); the controller toggles the canonical --active class instead
of swapping ad-hoc ACTIVE/INACTIVE class lists. The client-side nature switch
(expense<->income updates the form's hidden nature field without navigating) is
preserved -- verified live: switching to Income flips the active segment AND
sets the nature field to inflow.

* fix(ds): reports period tabs -> DS::SegmentedControl

The Monthly/Quarterly/YTD/Last-6-Months/Custom period selector was five
DS::Link ghost/secondary buttons -- a different tab idiom than the rest of the
app. Migrate to DS::SegmentedControl link segments (server-rendered active via
aria-current; pure navigation, no controller). Now matches the goals / provider /
transaction-type controls + the AI provider picker. Also autocorrected a
pre-existing single-quote in the next-period aria-label.
This commit is contained in:
Guillem Arias Fauste
2026-06-11 15:47:00 +02:00
committed by GitHub
parent 2defccf366
commit d29591cff9
7 changed files with 67 additions and 101 deletions

View File

@@ -155,10 +155,7 @@ export default class extends Controller {
this.chipTargets.forEach((chip) => {
const active = chip.dataset.status === this.statusValue;
chip.setAttribute("aria-pressed", active);
chip.classList.toggle("bg-container", active);
chip.classList.toggle("shadow-border-xs", active);
chip.classList.toggle("text-primary", active);
chip.classList.toggle("text-secondary", !active);
chip.classList.toggle("segmented-control__segment--active", active);
});
}
}

View File

@@ -58,10 +58,7 @@ export default class extends Controller {
if (!this.hasChipTarget) return;
this.chipTargets.forEach((chip) => {
const active = chip.dataset.kind === this.kindValue;
chip.classList.toggle("bg-container", active);
chip.classList.toggle("shadow-border-xs", active);
chip.classList.toggle("text-primary", active);
chip.classList.toggle("text-secondary", !active);
chip.classList.toggle("segmented-control__segment--active", active);
chip.setAttribute("aria-pressed", active ? "true" : "false");
});
}

View File

@@ -1,21 +1,26 @@
import { Controller } from "@hotwired/stimulus"
const ACTIVE_CLASSES = ["bg-container", "text-primary", "shadow-sm"]
const INACTIVE_CLASSES = ["hover:bg-container", "text-subdued", "hover:text-primary", "hover:shadow-sm"]
import { Controller } from "@hotwired/stimulus";
// Expense / income tabs render as a DS::SegmentedControl. Switching is
// client-side here: it updates the form's hidden nature field and flips the
// active segment without navigating (the href is a progressive-enhancement
// fallback). Transfer is a plain link to the transfer form.
export default class extends Controller {
static targets = ["tab", "natureField"]
static targets = ["tab", "natureField"];
selectTab(event) {
event.preventDefault()
event.preventDefault();
const selectedTab = event.currentTarget
this.natureFieldTarget.value = selectedTab.dataset.nature
const selectedTab = event.currentTarget;
this.natureFieldTarget.value = selectedTab.dataset.nature;
this.tabTargets.forEach(tab => {
const isActive = tab === selectedTab
tab.classList.remove(...(isActive ? INACTIVE_CLASSES : ACTIVE_CLASSES))
tab.classList.add(...(isActive ? ACTIVE_CLASSES : INACTIVE_CLASSES))
})
this.tabTargets.forEach((tab) => {
const isActive = tab === selectedTab;
tab.classList.toggle("segmented-control__segment--active", isActive);
if (isActive) {
tab.setAttribute("aria-current", "true");
} else {
tab.removeAttribute("aria-current");
}
});
}
}