diff --git a/app/components/menu_component.html.erb b/app/components/menu_component.html.erb
index 7560d317..527e5e36 100644
--- a/app/components/menu_component.html.erb
+++ b/app/components/menu_component.html.erb
@@ -1,4 +1,4 @@
-<%= tag.div data: { controller: "menu", menu_placement_value: placement, menu_offset_value: offset } do %>
+<%= tag.div data: { controller: "menu", menu_placement_value: placement, menu_offset_value: offset, testid: testid } do %>
<% if variant == :icon %>
<%= render ButtonComponent.new(variant: "icon", icon: icon_vertical ? "more-vertical" : "more-horizontal", data: { menu_target: "button" }) %>
<% elsif variant == :button %>
@@ -11,15 +11,17 @@
<% end %>
-
- <%= header %>
+
+
+ <%= header %>
-
- <% items.each do |item| %>
- <%= item %>
+ <%= tag.div class: class_names("py-1" => !no_padding) do %>
+ <% items.each do |item| %>
+ <%= item %>
+ <% end %>
+
+ <%= custom_content %>
<% end %>
-
- <%= custom_content %>
<% end %>
diff --git a/app/components/menu_component.rb b/app/components/menu_component.rb
index 5e3a2472..012b2f62 100644
--- a/app/components/menu_component.rb
+++ b/app/components/menu_component.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class MenuComponent < ViewComponent::Base
- attr_reader :variant, :avatar_url, :placement, :offset, :icon_vertical
+ attr_reader :variant, :avatar_url, :placement, :offset, :icon_vertical, :no_padding, :testid
renders_one :button, ->(**button_options, &block) do
options_with_target = button_options.merge(data: { menu_target: "button" })
@@ -23,12 +23,14 @@ class MenuComponent < ViewComponent::Base
VARIANTS = %i[icon button avatar].freeze
- def initialize(variant: "icon", avatar_url: nil, placement: "bottom-end", offset: 12, icon_vertical: false)
+ def initialize(variant: "icon", avatar_url: nil, placement: "bottom-end", offset: 12, icon_vertical: false, no_padding: false, testid: nil)
@variant = variant.to_sym
@avatar_url = avatar_url
@placement = placement
@offset = offset
@icon_vertical = icon_vertical
+ @no_padding = no_padding
+ @testid = testid
raise ArgumentError, "Invalid variant: #{@variant}" unless VARIANTS.include?(@variant)
end
diff --git a/app/javascript/controllers/menu_controller.js b/app/components/menu_controller.js
similarity index 100%
rename from app/javascript/controllers/menu_controller.js
rename to app/components/menu_controller.js
diff --git a/app/components/tabs/nav_component.rb b/app/components/tabs/nav_component.rb
new file mode 100644
index 00000000..2c4e81ca
--- /dev/null
+++ b/app/components/tabs/nav_component.rb
@@ -0,0 +1,29 @@
+class Tabs::NavComponent < ViewComponent::Base
+ erb_template <<~ERB
+ <%= tag.nav class: classes do %>
+ <% btns.each do |btn| %>
+ <%= btn %>
+ <% end %>
+ <% end %>
+ ERB
+
+ renders_many :btns, ->(id:, label:, classes: nil, &block) do
+ content_tag(
+ :button, label, id: id,
+ type: "button",
+ class: class_names(btn_classes, id == active_tab ? active_btn_classes : inactive_btn_classes, classes),
+ data: { id: id, action: "tabs#show", tabs_target: "navBtn" },
+ &block
+ )
+ end
+
+ attr_reader :active_tab, :classes, :active_btn_classes, :inactive_btn_classes, :btn_classes
+
+ def initialize(active_tab:, classes: nil, active_btn_classes: nil, inactive_btn_classes: nil, btn_classes: nil)
+ @active_tab = active_tab
+ @classes = classes
+ @active_btn_classes = active_btn_classes
+ @inactive_btn_classes = inactive_btn_classes
+ @btn_classes = btn_classes
+ end
+end
diff --git a/app/components/tabs/panel_component.rb b/app/components/tabs/panel_component.rb
new file mode 100644
index 00000000..3c34932a
--- /dev/null
+++ b/app/components/tabs/panel_component.rb
@@ -0,0 +1,11 @@
+class Tabs::PanelComponent < ViewComponent::Base
+ attr_reader :tab_id
+
+ def initialize(tab_id:)
+ @tab_id = tab_id
+ end
+
+ def call
+ content
+ end
+end
diff --git a/app/components/tabs_component.html.erb b/app/components/tabs_component.html.erb
index 1ac0e78f..4ec901fa 100644
--- a/app/components/tabs_component.html.erb
+++ b/app/components/tabs_component.html.erb
@@ -1,22 +1,17 @@
-<%= tag.div class: "space-y-4", data: {
+<%= tag.div data: {
controller: "tabs",
+ testid: testid,
tabs_url_param_key_value: url_param_key,
- tabs_nav_btn_active_class: nav_btn_active_classes,
- tabs_nav_btn_inactive_class: nav_btn_inactive_classes
+ tabs_nav_btn_active_class: active_btn_classes,
+ tabs_nav_btn_inactive_class: inactive_btn_classes
} do %>
-
+ <% if unstyled? %>
+ <%= content %>
+ <% else %>
+ <%= nav %>
-
- <% tabs.each do |tab| %>
- <%= tag.div class: class_names("hidden" => tab.id != active_tab), data: { id: tab.id, tabs_target: "panel" } do %>
- <%= tab %>
- <% end %>
+ <% panels.each do |panel| %>
+ <%= panel %>
<% end %>
-
-<% end %>
\ No newline at end of file
+ <% end %>
+<% end %>
diff --git a/app/components/tabs_component.rb b/app/components/tabs_component.rb
index 6c6735f4..4017b308 100644
--- a/app/components/tabs_component.rb
+++ b/app/components/tabs_component.rb
@@ -1,38 +1,65 @@
class TabsComponent < ViewComponent::Base
- renders_many :tabs, TabComponent
+ renders_one :nav, ->(classes: nil) do
+ Tabs::NavComponent.new(
+ active_tab: active_tab,
+ active_btn_classes: active_btn_classes,
+ inactive_btn_classes: inactive_btn_classes,
+ btn_classes: base_btn_classes,
+ classes: unstyled? ? classes : class_names(nav_container_classes, classes)
+ )
+ end
+
+ renders_many :panels, ->(tab_id:, &block) do
+ content_tag(
+ :div,
+ class: ("hidden" unless tab_id == active_tab),
+ data: { id: tab_id, tabs_target: "panel" },
+ &block
+ )
+ end
VARIANTS = {
default: {
- nav_btn_active_classes: "bg-white theme-dark:bg-gray-700 text-primary shadow-sm",
- nav_btn_inactive_classes: "text-secondary hover:bg-surface-inset-hover",
- nav_btn_classes: "w-full inline-flex justify-center items-center text-sm font-medium px-2 py-1 rounded-md transition-colors duration-200"
+ active_btn_classes: "bg-white theme-dark:bg-gray-700 text-primary shadow-sm",
+ inactive_btn_classes: "text-secondary hover:bg-surface-inset-hover",
+ base_btn_classes: "w-full inline-flex justify-center items-center text-sm font-medium px-2 py-1 rounded-md transition-colors duration-200",
+ nav_container_classes: "flex bg-surface-inset p-1 rounded-lg mb-4"
}
}
- attr_reader :variant, :url_param_key
+ attr_reader :active_tab, :url_param_key, :variant, :testid
- def initialize(active_tab: nil, variant: "default", url_param_key: nil)
+ def initialize(active_tab:, url_param_key: nil, variant: :default, active_btn_classes: "", inactive_btn_classes: "", testid: nil)
@active_tab = active_tab
- @variant = variant.to_sym
@url_param_key = url_param_key
+ @variant = variant.to_sym
+ @active_btn_classes = active_btn_classes
+ @inactive_btn_classes = inactive_btn_classes
+ @testid = testid
end
- def active_tab
- @active_tab || tabs.first.id
+ def active_btn_classes
+ unstyled? ? @active_btn_classes : VARIANTS.dig(variant, :active_btn_classes)
end
- def nav_btn_active_classes
- VARIANTS.dig(variant, :nav_btn_active_classes)
+ def inactive_btn_classes
+ unstyled? ? @inactive_btn_classes : VARIANTS.dig(variant, :inactive_btn_classes)
end
- def nav_btn_inactive_classes
- VARIANTS.dig(variant, :nav_btn_inactive_classes)
- end
+ private
+ def unstyled?
+ variant == :unstyled
+ end
- def nav_btn_classes(active: false)
- class_names(
- VARIANTS.dig(variant, :nav_btn_classes),
- active ? nav_btn_active_classes : nav_btn_inactive_classes
- )
- end
+ def base_btn_classes
+ unless unstyled?
+ VARIANTS.dig(variant, :base_btn_classes)
+ end
+ end
+
+ def nav_container_classes
+ unless unstyled?
+ VARIANTS.dig(variant, :nav_container_classes)
+ end
+ end
end
diff --git a/app/components/tabs_controller.js b/app/components/tabs_controller.js
new file mode 100644
index 00000000..32f18d08
--- /dev/null
+++ b/app/components/tabs_controller.js
@@ -0,0 +1,42 @@
+import { Controller } from "@hotwired/stimulus";
+
+// Connects to data-controller="tabs--components"
+export default class extends Controller {
+ static classes = ["navBtnActive", "navBtnInactive"];
+ static targets = ["panel", "navBtn"];
+ static values = { urlParamKey: String };
+
+ connect() {
+ console.log("tabs controller connected");
+ }
+
+ show(e) {
+ const btn = e.target.closest("button");
+ const selectedTabId = btn.dataset.id;
+
+ this.navBtnTargets.forEach((navBtn) => {
+ if (navBtn.dataset.id === selectedTabId) {
+ navBtn.classList.add(...this.navBtnActiveClasses);
+ navBtn.classList.remove(...this.navBtnInactiveClasses);
+ } else {
+ navBtn.classList.add(...this.navBtnInactiveClasses);
+ navBtn.classList.remove(...this.navBtnActiveClasses);
+ }
+ });
+
+ this.panelTargets.forEach((panel) => {
+ if (panel.dataset.id === selectedTabId) {
+ panel.classList.remove("hidden");
+ } else {
+ panel.classList.add("hidden");
+ }
+ });
+
+ // Update URL with the selected tab
+ if (this.urlParamKeyValue) {
+ const url = new URL(window.location.href);
+ url.searchParams.set(this.urlParamKeyValue, selectedTabId);
+ window.history.replaceState({}, "", url);
+ }
+ }
+}
diff --git a/app/components/toggle_component.html.erb b/app/components/toggle_component.html.erb
index 1fd6a428..6845686c 100644
--- a/app/components/toggle_component.html.erb
+++ b/app/components/toggle_component.html.erb
@@ -1,5 +1,5 @@
<%= hidden_field_tag name, unchecked_value, id: nil %>
- <%= check_box_tag name, checked_value, checked, class: "sr-only peer", disabled: disabled, id: id, **opts %>
+ <%= check_box_tag name, checked_value, checked, class: "sr-only peer", disabled: disabled, id: id, **opts %>
<%= label_tag name, " ".html_safe, class: label_classes, for: id %>
-
\ No newline at end of file
+
diff --git a/app/helpers/transactions_helper.rb b/app/helpers/transactions_helper.rb
index dd729c47..173306b9 100644
--- a/app/helpers/transactions_helper.rb
+++ b/app/helpers/transactions_helper.rb
@@ -1,13 +1,13 @@
module TransactionsHelper
def transaction_search_filters
[
- { key: "account_filter", icon: "layers" },
- { key: "date_filter", icon: "calendar" },
- { key: "type_filter", icon: "tag" },
- { key: "amount_filter", icon: "hash" },
- { key: "category_filter", icon: "shapes" },
- { key: "tag_filter", icon: "tags" },
- { key: "merchant_filter", icon: "store" }
+ { key: "account_filter", label: "Account", icon: "layers" },
+ { key: "date_filter", label: "Date", icon: "calendar" },
+ { key: "type_filter", label: "Type", icon: "tag" },
+ { key: "amount_filter", label: "Amount", icon: "hash" },
+ { key: "category_filter", label: "Category", icon: "shapes" },
+ { key: "tag_filter", label: "Tag", icon: "tags" },
+ { key: "merchant_filter", label: "Merchant", icon: "store" }
]
end
diff --git a/app/javascript/controllers/tabs_controller.js b/app/javascript/controllers/tabs_controller.js
deleted file mode 100644
index 557d5e52..00000000
--- a/app/javascript/controllers/tabs_controller.js
+++ /dev/null
@@ -1,107 +0,0 @@
-import { Controller } from "@hotwired/stimulus";
-
-// Connects to data-controller="tabs"
-export default class extends Controller {
- static classes = ["active", "inactive", "navBtnActive", "navBtnInactive"];
- static targets = ["btn", "tab", "panel", "navBtn"];
- static values = {
- defaultTab: String,
- localStorageKey: String,
- urlParamKey: String,
- };
-
- connect() {
- const selectedTab = this.hasLocalStorageKeyValue
- ? this.getStoredTab() || this.defaultTabValue
- : this.defaultTabValue;
-
- this.updateClasses(selectedTab);
- document.addEventListener("turbo:load", this.onTurboLoad);
- }
-
- disconnect() {
- document.removeEventListener("turbo:load", this.onTurboLoad);
- }
-
- select(event) {
- const element = event.target.closest("[data-id]");
- if (element) {
- const selectedId = element.dataset.id;
- this.updateClasses(selectedId);
- if (this.hasLocalStorageKeyValue) {
- this.storeTab(selectedId);
- }
- }
- }
-
- show(e) {
- const selectedTabId = e.target.dataset.id;
-
- this.navBtnTargets.forEach((navBtn) => {
- if (navBtn.dataset.id === selectedTabId) {
- navBtn.classList.add(...this.navBtnActiveClasses);
- navBtn.classList.remove(...this.navBtnInactiveClasses);
- } else {
- navBtn.classList.add(...this.navBtnInactiveClasses);
- navBtn.classList.remove(...this.navBtnActiveClasses);
- }
- });
-
- this.panelTargets.forEach((panel) => {
- if (panel.dataset.id === selectedTabId) {
- panel.classList.remove("hidden");
- } else {
- panel.classList.add("hidden");
- }
- });
-
- // Update URL with the selected tab
- if (this.urlParamKeyValue) {
- const url = new URL(window.location.href);
- url.searchParams.set(this.urlParamKeyValue, selectedTabId);
- window.history.replaceState({}, "", url);
- }
- }
-
- onTurboLoad = () => {
- const selectedTab = this.hasLocalStorageKeyValue
- ? this.getStoredTab() || this.defaultTabValue
- : this.defaultTabValue;
-
- this.updateClasses(selectedTab);
- };
-
- getStoredTab() {
- const tabs = JSON.parse(localStorage.getItem("tabs") || "{}");
- return tabs[this.localStorageKeyValue];
- }
-
- storeTab(selectedId) {
- const tabs = JSON.parse(localStorage.getItem("tabs") || "{}");
- tabs[this.localStorageKeyValue] = selectedId;
- localStorage.setItem("tabs", JSON.stringify(tabs));
- }
-
- updateClasses = (selectedId) => {
- this.btnTargets.forEach((btn) => {
- btn.classList.remove(...this.activeClasses);
- btn.classList.remove(...this.inactiveClasses);
- });
-
- this.tabTargets.forEach((tab) => tab.classList.add("hidden"));
-
- this.btnTargets.forEach((btn) => {
- if (btn.dataset.id === selectedId) {
- btn.classList.add(...this.activeClasses);
- } else {
- btn.classList.add(...this.inactiveClasses);
- }
- });
-
- this.tabTargets.forEach((tab) => {
- if (tab.id === selectedId) {
- tab.classList.remove("hidden");
- }
- });
- };
-}
diff --git a/app/views/accounts/_account_sidebar_tabs.html.erb b/app/views/accounts/_account_sidebar_tabs.html.erb
index 416d681b..a8918de8 100644
--- a/app/views/accounts/_account_sidebar_tabs.html.erb
+++ b/app/views/accounts/_account_sidebar_tabs.html.erb
@@ -21,59 +21,71 @@
<% end %>
- <%= render TabsComponent.new(active_tab: active_account_group_tab, url_param_key: "account_group_tab") do |container| %>
- <% container.with_tab(id: "assets", label: "Assets") do %>
- <%= render LinkComponent.new(
- text: "New asset",
- variant: "ghost",
- href: new_account_path(step: "method_select", classification: "asset"),
- icon: "plus",
- frame: :modal,
- full_width: true,
- class: "justify-start"
- ) %>
+ <%= render TabsComponent.new(active_tab: active_account_group_tab, url_param_key: "account_group_tab", testid: "account-sidebar-tabs") do |tabs| %>
+ <% tabs.with_nav do |nav| %>
+ <% nav.with_btn(id: "assets", label: "Assets") %>
+ <% nav.with_btn(id: "debts", label: "Debts") %>
+ <% nav.with_btn(id: "all", label: "All") %>
+ <% end %>
+ <% tabs.with_panel(tab_id: "assets") do %>
- <% family.balance_sheet.account_groups("asset").each do |group| %>
- <%= render "accounts/accountable_group", account_group: group %>
- <% end %>
+ <%= render LinkComponent.new(
+ text: "New asset",
+ variant: "ghost",
+ href: new_account_path(step: "method_select", classification: "asset"),
+ icon: "plus",
+ frame: :modal,
+ full_width: true,
+ class: "justify-start"
+ ) %>
+
+
+ <% family.balance_sheet.account_groups("asset").each do |group| %>
+ <%= render "accounts/accountable_group", account_group: group %>
+ <% end %>
+
<% end %>
- <% container.with_tab(id: "debts", label: "Debts") do %>
- <%= render LinkComponent.new(
- text: "New debt",
- variant: "ghost",
- href: new_account_path(step: "method_select", classification: "liability"),
- icon: "plus",
- frame: :modal,
- full_width: true,
- class: "justify-start"
- ) %>
-
+ <% tabs.with_panel(tab_id: "debts") do %>
- <% family.balance_sheet.account_groups("liability").each do |group| %>
- <%= render "accounts/accountable_group", account_group: group %>
- <% end %>
+ <%= render LinkComponent.new(
+ text: "New debt",
+ variant: "ghost",
+ href: new_account_path(step: "method_select", classification: "liability"),
+ icon: "plus",
+ frame: :modal,
+ full_width: true,
+ class: "justify-start"
+ ) %>
+
+
+ <% family.balance_sheet.account_groups("liability").each do |group| %>
+ <%= render "accounts/accountable_group", account_group: group %>
+ <% end %>
+
<% end %>
- <% container.with_tab(id: "all", label: "All") do %>
- <%= render LinkComponent.new(
- text: "New account",
- variant: "ghost",
- full_width: true,
- href: new_account_path(step: "method_select"),
- icon: "plus",
- frame: :modal,
- class: "justify-start"
- ) %>
-
+ <% tabs.with_panel(tab_id: "all") do %>
- <% family.balance_sheet.account_groups.each do |group| %>
- <%= render "accounts/accountable_group", account_group: group %>
- <% end %>
+ <%= render LinkComponent.new(
+ text: "New account",
+ variant: "ghost",
+ full_width: true,
+ href: new_account_path(step: "method_select"),
+ icon: "plus",
+ frame: :modal,
+ class: "justify-start"
+ ) %>
+
+
+ <% family.balance_sheet.account_groups.each do |group| %>
+ <%= render "accounts/accountable_group", account_group: group %>
+ <% end %>
+
<% end %>
<% end %>
-
\ No newline at end of file
+
diff --git a/app/views/accounts/show/_menu.html.erb b/app/views/accounts/show/_menu.html.erb
index 41eaeb29..5e59c88a 100644
--- a/app/views/accounts/show/_menu.html.erb
+++ b/app/views/accounts/show/_menu.html.erb
@@ -1,6 +1,6 @@
<%# locals: (account:) %>
-<%= render MenuComponent.new do |menu| %>
+<%= render MenuComponent.new(testid: "account-menu") do |menu| %>
<% menu.with_item(variant: "link", text: "Edit", href: edit_account_path(account), icon: "pencil-line", data: { turbo_frame: :modal }) %>
<% unless account.crypto? %>
diff --git a/app/views/accounts/show/_tabs.html.erb b/app/views/accounts/show/_tabs.html.erb
index 2a0e2b2f..169fe0d4 100644
--- a/app/views/accounts/show/_tabs.html.erb
+++ b/app/views/accounts/show/_tabs.html.erb
@@ -1,11 +1,17 @@
<%# locals: (account:, tabs:) %>
-<% selected_tab = tabs.find { |tab| tab[:key] == params[:tab] } || tabs.first %>
+<% active_tab = tabs.find { |tab| tab[:key] == params[:tab] } || tabs.first %>
-
- <% tabs.each do |tab| %>
- <%= render "accounts/show/tab", account: account, key: tab[:key], is_selected: selected_tab[:key] == tab[:key] %>
+<%= render TabsComponent.new(active_tab: active_tab[:key], url_param_key: "tab") do |tabs_container| %>
+ <% tabs_container.with_nav(classes: "max-w-fit") do |nav| %>
+ <% tabs.each do |tab| %>
+ <% nav.with_btn(id: tab[:key], label: tab[:key].humanize, classes: "px-6") %>
+ <% end %>
<% end %>
-
-<%= selected_tab[:contents] %>
+ <% tabs.each do |tab| %>
+ <% tabs_container.with_panel(tab_id: tab[:key]) do %>
+ <%= tab[:contents] %>
+ <% end %>
+ <% end %>
+<% end %>
diff --git a/app/views/accounts/show/_template.html.erb b/app/views/accounts/show/_template.html.erb
index 5bc44376..20d352b5 100644
--- a/app/views/accounts/show/_template.html.erb
+++ b/app/views/accounts/show/_template.html.erb
@@ -16,7 +16,7 @@
<%= render "accounts/show/chart", account: account %>
<% end %>
-