module ApplicationHelper
include Pagy::Frontend
def icon(key, size: "md", color: "current")
render partial: "shared/icon", locals: { key:, size:, color: }
end
def icon_custom(key, size: "md", color: "current")
render partial: "shared/icon_custom", locals: { key:, size:, color: }
end
# Convert alpha (0-1) to 8-digit hex (00-FF)
def hex_with_alpha(hex, alpha)
alpha_hex = (alpha * 255).round.to_s(16).rjust(2, "0")
"#{hex}#{alpha_hex}"
end
def title(page_title)
content_for(:title) { page_title }
end
def header_title(page_title)
content_for(:header_title) { page_title }
end
def header_description(page_description)
content_for(:header_description) { page_description }
end
def family_stream
turbo_stream_from Current.family if Current.family
end
##
# Helper to open a centered and overlayed modal with custom contents
#
# @example Basic usage
# <%= modal classes: "custom-class" do %>
#
Content here
# <% end %>
#
def modal(reload_on_close: false, overflow_visible: false, &block)
content = capture &block
render partial: "shared/modal", locals: { content:, reload_on_close:, overflow_visible: }
end
##
# Helper to open a drawer on the right side of the screen with custom contents
#
# @example Basic usage
# <%= drawer do %>
# Content here
# <% end %>
#
def drawer(reload_on_close: false, &block)
content = capture &block
render partial: "shared/drawer", locals: { content:, reload_on_close: }
end
def disclosure(title, default_open: true, &block)
content = capture &block
render partial: "shared/disclosure", locals: { title: title, content: content, open: default_open }
end
def page_active?(path)
current_page?(path) || (request.path.start_with?(path) && path != "/")
end
def mixed_hex_styles(hex)
color = hex || "#1570EF" # blue-600
<<-STYLE.strip
background-color: color-mix(in srgb, #{color} 10%, white);
border-color: color-mix(in srgb, #{color} 30%, white);
color: #{color};
STYLE
end
def circle_logo(name, hex: nil, size: "md")
render partial: "shared/circle_logo", locals: { name: name, hex: hex, size: size }
end
def return_to_path(params, fallback = root_path)
uri = URI.parse(params[:return_to] || fallback)
uri.relative? ? uri.path : root_path
end
# Wrapper around I18n.l to support custom date formats
def format_date(object, format = :default, options = {})
date = object.to_date
format_code = options[:format_code] || Current.family&.date_format
if format_code.present?
date.strftime(format_code)
else
I18n.l(date, format: format, **options)
end
end
def format_money(number_or_money, options = {})
return nil unless number_or_money
Money.new(number_or_money).format(options)
end
def totals_by_currency(collection:, money_method:, separator: " | ", negate: false)
collection.group_by(&:currency)
.transform_values { |item| calculate_total(item, money_method, negate) }
.map { |_currency, money| format_money(money) }
.join(separator)
end
def show_super_admin_bar?
if params[:admin].present?
cookies.permanent[:admin] = params[:admin]
end
cookies[:admin] == "true"
end
# Renders Markdown text using Redcarpet
def markdown(text)
return "" if text.blank?
renderer = Redcarpet::Render::HTML.new(
hard_wrap: true,
link_attributes: { target: "_blank", rel: "noopener noreferrer" }
)
markdown = Redcarpet::Markdown.new(
renderer,
autolink: true,
tables: true,
fenced_code_blocks: true,
strikethrough: true,
superscript: true,
underline: true,
highlight: true,
quote: true,
footnotes: true
)
markdown.render(text).html_safe
end
# Determines the starting widths of each panel depending on the user's sidebar preferences
def app_sidebar_config(user)
left_sidebar_showing = user.show_sidebar?
right_sidebar_showing = user.show_ai_sidebar?
content_max_width = if !left_sidebar_showing && !right_sidebar_showing
1024 # 5xl
elsif left_sidebar_showing && !right_sidebar_showing
896 # 4xl
else
768 # 3xl
end
left_panel_min_width = 320
left_panel_max_width = 320
right_panel_min_width = 400
right_panel_max_width = 550
left_panel_width = left_sidebar_showing ? left_panel_min_width : 0
right_panel_width = if right_sidebar_showing
left_sidebar_showing ? right_panel_min_width : right_panel_max_width
else
0
end
{
left_panel: {
is_open: left_sidebar_showing,
initial_width: left_panel_width,
min_width: left_panel_min_width,
max_width: left_panel_max_width
},
right_panel: {
is_open: right_sidebar_showing,
initial_width: right_panel_width,
min_width: right_panel_min_width,
max_width: right_panel_max_width,
overflow: right_sidebar_showing ? "auto" : "hidden"
},
content_max_width: content_max_width
}
end
private
def calculate_total(item, money_method, negate)
items = item.reject { |i| i.respond_to?(:entryable) && i.entryable.transfer? }
total = items.sum(&money_method)
negate ? -total : total
end
end