diff --git a/app/javascript/controllers/goals_filter_controller.js b/app/javascript/controllers/goals_filter_controller.js index 5e68db6f8..ed98040b9 100644 --- a/app/javascript/controllers/goals_filter_controller.js +++ b/app/javascript/controllers/goals_filter_controller.js @@ -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); }); } } diff --git a/app/javascript/controllers/providers_filter_controller.js b/app/javascript/controllers/providers_filter_controller.js index a4d90e2a4..3b07a0f45 100644 --- a/app/javascript/controllers/providers_filter_controller.js +++ b/app/javascript/controllers/providers_filter_controller.js @@ -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"); }); } diff --git a/app/javascript/controllers/transaction_type_tabs_controller.js b/app/javascript/controllers/transaction_type_tabs_controller.js index e347121de..ca16a0459 100644 --- a/app/javascript/controllers/transaction_type_tabs_controller.js +++ b/app/javascript/controllers/transaction_type_tabs_controller.js @@ -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"); + } + }); } } diff --git a/app/views/goals/index.html.erb b/app/views/goals/index.html.erb index 8d927234b..d0bb68b10 100644 --- a/app/views/goals/index.html.erb +++ b/app/views/goals/index.html.erb @@ -125,19 +125,18 @@ horizontally when the column is too narrow for all six chips, rather than wrapping "On track" mid-label or forcing the row wider than the viewport. %> -
+ <%= render DS::SegmentedControl.new(aria_label: t(".chips_aria", default: "Filter goals by status"), class: "overflow-x-auto max-w-full") do |sc| %> <% %w[all on_track behind no_target_date paused completed].each do |status| %> - <% active = status == "all" %> - + <% sc.with_segment(t(".chips.#{status}"), + active: status == "all", + class: "shrink-0 whitespace-nowrap", + data: { + "goals-filter-target": "chip", + status: status, + action: "click->goals-filter#selectChip" + }) %> <% end %> -
+ <% end %> <% end %> diff --git a/app/views/reports/index.html.erb b/app/views/reports/index.html.erb index 8080b6b2e..a01f68cc4 100644 --- a/app/views/reports/index.html.erb +++ b/app/views/reports/index.html.erb @@ -30,38 +30,14 @@ <%# Period Navigation Tabs %>
-
- <%= render DS::Link.new( - text: t("reports.index.periods.monthly"), - variant: @period_type == :monthly ? "secondary" : "ghost", - href: reports_path(period_type: :monthly), - size: :sm - ) %> - <%= render DS::Link.new( - text: t("reports.index.periods.quarterly"), - variant: @period_type == :quarterly ? "secondary" : "ghost", - href: reports_path(period_type: :quarterly), - size: :sm - ) %> - <%= render DS::Link.new( - text: t("reports.index.periods.ytd"), - variant: @period_type == :ytd ? "secondary" : "ghost", - href: reports_path(period_type: :ytd), - size: :sm - ) %> - <%= render DS::Link.new( - text: t("reports.index.periods.last_6_months"), - variant: @period_type == :last_6_months ? "secondary" : "ghost", - href: reports_path(period_type: :last_6_months), - size: :sm - ) %> - <%= render DS::Link.new( - text: t("reports.index.periods.custom"), - variant: @period_type == :custom ? "secondary" : "ghost", - href: reports_path(period_type: :custom), - size: :sm - ) %> -
+ <%= render DS::SegmentedControl.new(aria_label: t("reports.index.period_aria", default: "Report period"), class: "overflow-x-auto no-scrollbar") do |sc| %> + <% %i[monthly quarterly ytd last_6_months custom].each do |ptype| %> + <% sc.with_segment(t("reports.index.periods.#{ptype}"), + active: @period_type == ptype, + href: reports_path(period_type: ptype), + class: "whitespace-nowrap") %> + <% end %> + <% end %>
<%# Custom Date Range Picker (only shown when custom is selected) %> diff --git a/app/views/settings/providers/_search_filters.html.erb b/app/views/settings/providers/_search_filters.html.erb index 30c36b172..54d28f33f 100644 --- a/app/views/settings/providers/_search_filters.html.erb +++ b/app/views/settings/providers/_search_filters.html.erb @@ -8,17 +8,15 @@ action: "input->providers-filter#filter" } ) %> -
+ <%= render DS::SegmentedControl.new(aria_label: t("settings.providers.search_filters.aria_label")) do |sc| %> <% %w[all bank crypto investment].each do |kind| %> - <% active = kind == "all" %> - + <% sc.with_segment(t("settings.providers.search_filters.chips.#{kind}"), + active: kind == "all", + data: { + "providers-filter-target": "chip", + kind: kind, + action: "click->providers-filter#selectChip" + }) %> <% end %> -
+ <% end %> diff --git a/app/views/shared/_transaction_type_tabs.html.erb b/app/views/shared/_transaction_type_tabs.html.erb index dbb1f6b55..43b60a0da 100644 --- a/app/views/shared/_transaction_type_tabs.html.erb +++ b/app/views/shared/_transaction_type_tabs.html.erb @@ -1,30 +1,24 @@ <%# locals: (active_tab:, account_id: nil) %> -
- <%= link_to new_transaction_path(nature: "outflow", account_id: account_id), - data: { turbo_frame: :modal, action: "click->transaction-type-tabs#selectTab", nature: "outflow", "transaction-type-tabs-target": "tab" }, - class: "flex-1 min-w-0 flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-sm #{active_tab == 'expense' ? 'bg-container text-primary shadow-sm' : 'hover:bg-container text-subdued hover:text-primary hover:shadow-sm'}" do %> - <%= icon "minus-circle" %> - <%= tag.span class: "truncate" do %> - <%= t("shared.transaction_tabs.expense") %> - <% end %> - <% end %> +<%= render DS::SegmentedControl.new(full_width: true, aria_label: t("shared.transaction_tabs.aria", default: "Transaction type")) do |sc| %> + <% sc.with_segment( + safe_join([ icon("minus-circle"), tag.span(t("shared.transaction_tabs.expense"), class: "truncate") ]), + active: active_tab == "expense", + href: new_transaction_path(nature: "outflow", account_id: account_id), + class: "min-w-0 gap-2", + data: { turbo_frame: :modal, action: "click->transaction-type-tabs#selectTab", nature: "outflow", "transaction-type-tabs-target": "tab" }) %> - <%= link_to new_transaction_path(nature: "inflow", account_id: account_id), - data: { turbo_frame: :modal, action: "click->transaction-type-tabs#selectTab", nature: "inflow", "transaction-type-tabs-target": "tab" }, - class: "flex-1 min-w-0 flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-sm #{active_tab == 'income' ? 'bg-container text-primary shadow-sm' : 'hover:bg-container text-subdued hover:text-primary hover:shadow-sm'}" do %> - <%= icon "plus-circle" %> - <%= tag.span class: "truncate" do %> - <%= t("shared.transaction_tabs.income") %> - <% end %> - <% end %> + <% sc.with_segment( + safe_join([ icon("plus-circle"), tag.span(t("shared.transaction_tabs.income"), class: "truncate") ]), + active: active_tab == "income", + href: new_transaction_path(nature: "inflow", account_id: account_id), + class: "min-w-0 gap-2", + data: { turbo_frame: :modal, action: "click->transaction-type-tabs#selectTab", nature: "inflow", "transaction-type-tabs-target": "tab" }) %> - <%= link_to new_transfer_path(from_account_id: account_id), - data: { turbo_frame: :modal }, - class: "flex-1 min-w-0 flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-sm #{active_tab == 'transfer' ? 'bg-container text-primary shadow-sm' : 'hover:bg-container text-subdued hover:text-primary hover:shadow-sm'}" do %> - <%= icon "arrow-right-left" %> - <%= tag.span class: "truncate" do %> - <%= t("shared.transaction_tabs.transfer") %> - <% end %> - <% end %> -
+ <% sc.with_segment( + safe_join([ icon("arrow-right-left"), tag.span(t("shared.transaction_tabs.transfer"), class: "truncate") ]), + active: active_tab == "transfer", + href: new_transfer_path(from_account_id: account_id), + class: "min-w-0 gap-2", + data: { turbo_frame: :modal }) %> +<% end %>