diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 4793b2303..d46415e57 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -26,12 +26,14 @@ module ApplicationHelper
extra_classes
)
+ resolved_key = normalize_icon_key(key)
+
if custom
- inline_svg_tag("#{key}.svg", class: icon_classes, **opts)
+ inline_svg_tag("#{resolved_key}.svg", class: icon_classes, **opts)
elsif as_button
- render DS::Button.new(variant: "icon", class: extra_classes, icon: key, size: size, type: "button", **opts)
+ render DS::Button.new(variant: "icon", class: extra_classes, icon: resolved_key, size: size, type: "button", **opts)
else
- lucide_icon(key, class: icon_classes, **opts)
+ safe_lucide_icon(resolved_key, class: icon_classes, **opts)
end
end
@@ -190,6 +192,20 @@ module ApplicationHelper
end
private
+ def safe_lucide_icon(key, **opts)
+ lucide_icon(key, **opts)
+ rescue StandardError => e
+ Rails.logger.warn("[ApplicationHelper] Falling back to key for unknown icon #{key.inspect}: #{e.message}")
+ lucide_icon("key", **opts)
+ end
+
+ def normalize_icon_key(key)
+ normalized = key.to_s.strip
+ return normalized if normalized.blank?
+
+ normalized.downcase
+ end
+
def calculate_total(item, money_method, negate)
# Filter out transfer-type transactions from entries
# Only Entry objects have entryable transactions, Account objects don't
diff --git a/test/helpers/application_helper_test.rb b/test/helpers/application_helper_test.rb
index d946c5afd..434735449 100644
--- a/test/helpers/application_helper_test.rb
+++ b/test/helpers/application_helper_test.rb
@@ -1,6 +1,39 @@
require "test_helper"
class ApplicationHelperTest < ActionView::TestCase
+ test "#icon normalizes icon names to lowercase" do
+ capture = []
+
+ singleton_class.send(:define_method, :lucide_icon) do |key, **opts|
+ capture << [ key, opts ]
+ "".html_safe
+ end
+
+ icon("Key")
+
+ assert_equal "key", capture.first.first
+ ensure
+ singleton_class.send(:remove_method, :lucide_icon) if singleton_class.method_defined?(:lucide_icon)
+ end
+
+ test "#icon falls back when lucide icon is unknown" do
+ calls = []
+
+ singleton_class.send(:define_method, :lucide_icon) do |key, **_opts|
+ calls << key
+ raise ArgumentError, "Unknown icon #{key}" if key == "not-a-real-icon"
+
+ "".html_safe
+ end
+
+ result = icon("not-a-real-icon")
+
+ assert_equal [ "not-a-real-icon", "key" ], calls
+ assert_equal "", result
+ ensure
+ singleton_class.send(:remove_method, :lucide_icon) if singleton_class.method_defined?(:lucide_icon)
+ end
+
test "#title(page_title)" do
title("Test Title")
assert_equal "Test Title", content_for(:title)