mirror of
https://github.com/we-promise/sure.git
synced 2026-07-19 08:15:21 +00:00
* 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:
committed by
GitHub
parent
2defccf366
commit
d29591cff9
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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. %>
|
||||
<div class="inline-flex items-center gap-1 p-1 bg-surface-inset rounded-xl overflow-x-auto max-w-full">
|
||||
<%= 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" %>
|
||||
<button type="button"
|
||||
data-goals-filter-target="chip"
|
||||
data-action="click->goals-filter#selectChip"
|
||||
data-status="<%= status %>"
|
||||
aria-pressed="<%= active %>"
|
||||
class="shrink-0 whitespace-nowrap px-2.5 py-1 text-xs font-medium rounded-lg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-alpha-black-100 <%= active ? "bg-container shadow-border-xs text-primary" : "text-secondary" %>">
|
||||
<%= t(".chips.#{status}") %>
|
||||
</button>
|
||||
<% 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 %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
|
||||
@@ -30,38 +30,14 @@
|
||||
|
||||
<%# Period Navigation Tabs %>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-2 overflow-x-auto no-scrollbar">
|
||||
<%= 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
|
||||
) %>
|
||||
</div>
|
||||
<%= 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 %>
|
||||
</div>
|
||||
|
||||
<%# Custom Date Range Picker (only shown when custom is selected) %>
|
||||
|
||||
@@ -8,17 +8,15 @@
|
||||
action: "input->providers-filter#filter"
|
||||
}
|
||||
) %>
|
||||
<div class="inline-flex items-center gap-1 p-1 bg-surface-inset rounded-xl">
|
||||
<%= 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" %>
|
||||
<button type="button"
|
||||
data-providers-filter-target="chip"
|
||||
data-action="click->providers-filter#selectChip"
|
||||
data-kind="<%= kind %>"
|
||||
aria-pressed="<%= active %>"
|
||||
class="px-2.5 py-1 text-xs font-medium rounded-lg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-alpha-black-100 <%= active ? "bg-container shadow-border-xs text-primary" : "text-secondary" %>">
|
||||
<%= t("settings.providers.search_filters.chips.#{kind}") %>
|
||||
</button>
|
||||
<% 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 %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -1,30 +1,24 @@
|
||||
<%# locals: (active_tab:, account_id: nil) %>
|
||||
|
||||
<fieldset class="bg-surface-inset rounded-lg p-1 flex flex-wrap gap-2">
|
||||
<%= 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 %>
|
||||
</fieldset>
|
||||
<% 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 %>
|
||||
|
||||
Reference in New Issue
Block a user