1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-10 07:55:21 +02:00

Split into Link and Button components for clarity

This commit is contained in:
Zach Gollwitzer 2025-04-26 19:17:46 -04:00
parent cbd05ae3da
commit 1c9dd6ea5f
77 changed files with 738 additions and 579 deletions

View file

@ -6,6 +6,14 @@
}
}
@utility text-inverse {
@apply text-white;
@variant theme-dark {
@apply text-gray-900;
}
}
@utility text-secondary {
@apply text-gray-500;

View file

@ -1,15 +1,13 @@
<%= wrapper_tag do %>
<% if icon_only? %>
<%= lucide_icon(@icon, class: icon_classes) %>
<% else %>
<% if @leading_icon %>
<%= lucide_icon(@leading_icon, class: icon_classes) %>
<% end %>
<%= container do %>
<% if icon && (icon_position != "right") %>
<%= lucide_icon(icon, class: icon_classes) %>
<% end %>
<%= @text %>
<% unless icon_only? %>
<%= text %>
<% end %>
<% if @trailing_icon %>
<%= lucide_icon(@trailing_icon, class: icon_classes) %>
<% end %>
<% if icon && icon_position == "right" %>
<%= lucide_icon(icon, class: icon_classes) %>
<% end %>
<% end %>

View file

@ -1,149 +1,39 @@
# frozen_string_literal: true
class ButtonComponent < ViewComponent::Base
VARIANTS = {
primary: {
bg: "bg-inverse hover:bg-inverse-hover disabled:bg-gray-500 theme-dark:disabled:bg-gray-400",
text: "text-white theme-dark:text-gray-900",
icon: "fg-inverse"
},
secondary: {
bg: "bg-gray-50 theme-dark:bg-gray-700 hover:bg-gray-100 theme-dark:hover:bg-gray-600 disabled:bg-gray-200 theme-dark:disabled:bg-gray-600",
text: "text-gray-900 theme-dark:text-white",
icon: "fg-primary"
},
destructive: {
bg: "bg-red-500 theme-dark:bg-red-400 hover:bg-red-600 theme-dark:hover:bg-red-500 disabled:bg-red-200 theme-dark:disabled:bg-red-600",
text: "text-white theme-dark:text-white",
icon: "fg-white"
},
outline: {
bg: "bg-transparent hover:bg-surface-hover",
text: "text-gray-900 theme-dark:text-white",
border: "border border-secondary",
icon: "fg-gray"
},
outline_destructive: {
bg: "bg-transparent hover:bg-gray-100 theme-dark:hover:bg-gray-700",
text: "text-destructive",
border: "border border-secondary"
},
ghost: {
bg: "bg-transparent hover:bg-gray-100 theme-dark:hover:bg-gray-700",
text: "text-primary",
icon: "fg-gray"
},
link_color: {
bg: "bg-transparent hover:bg-gray-100 theme-dark:hover:bg-gray-700",
text: "text-primary",
icon: "fg-inverse"
},
link_gray: {
bg: "bg-transparent hover:bg-gray-100 theme-dark:hover:bg-gray-700",
text: "text-secondary",
icon: "fg-gray"
},
icon: {
bg: "bg-transparent hover:bg-gray-100 theme-dark:hover:bg-gray-700 rounded-lg",
text: "text-secondary",
icon: "fg-gray"
},
icon_inverse: {
bg: "bg-inverse hover:bg-inverse-hover rounded-lg",
text: "fg-inverse",
icon: "fg-inverse"
}
}.freeze
include ButtonStylable
SIZES = {
sm: {
icon_container: "w-8 h-8",
container: "px-2 py-1 rounded-md",
text: "text-sm",
icon: "w-4 h-4"
},
md: {
icon_container: "w-9 h-9",
container: "px-3 py-2 rounded-lg",
text: "text-sm",
icon: "w-5 h-5"
},
lg: {
icon_container: "w-10 h-10",
container: "px-4 py-3 rounded-xl",
text: "text-base",
icon: "w-6 h-6"
}
}
attr_reader :text, :icon, :icon_position
def initialize(options = {})
@text = options.delete(:text)
@variant = (options.delete(:variant) || "primary").underscore.to_sym
@size = (options.delete(:size) || :md).to_sym
@href = options.delete(:href)
@method = options.delete(:method) || :get
@leading_icon = options.delete(:leading_icon)
@trailing_icon = options.delete(:trailing_icon)
@icon = options.delete(:icon)
@full_width = options.delete(:full_width)
@left_align = options.delete(:left_align)
@extra_classes = options.delete(:class)
@options = options
def initialize(
text: nil, variant: "primary", size: "md", icon: nil, icon_position: "left", full_width: false, rounded: false, **opts
)
@text = text
@variant = variant.underscore.to_sym
@size = size.to_sym
@icon = icon
@icon_position = icon_position
@full_width = full_width
@rounded = rounded
@opts = opts
end
def wrapper_tag(&block)
if @href && @method != :get
button_to @href, class: container_classes, method: @method, **@options, &block
elsif @href
link_to @href, class: container_classes, **@options, &block
def container(&block)
merged_opts = opts.dup || {}
extra_classes = merged_opts.delete(:class)
href = merged_opts.delete(:href)
merged_opts = merged_opts.merge(
class: class_names(container_classes, extra_classes)
)
if href.present?
button_to(href, **merged_opts, &block)
else
content_tag :button, type: "button", class: container_classes, **@options, &block
content_tag(:button, **merged_opts, &block)
end
end
def icon_classes
[
"shrink-0",
size_meta[:icon],
variant_meta[:icon]
].join(" ")
end
def icon_only?
@variant == :icon || @variant == :icon_inverse
end
private
def container_classes
hidden_override = (@extra_classes || "").split(" ").include?("hidden")
default_classes = hidden_override ? "items-center gap-1" : "inline-flex items-center gap-1"
[
"whitespace-nowrap",
default_classes,
@full_width ? "w-full" : nil,
@left_align ? "justify-start" : "justify-center",
icon_only? ? size_meta[:icon_container] : size_meta[:container],
variant_meta[:bg],
variant_meta.dig(:border),
text_classes,
@extra_classes
].compact.join(" ")
end
def text_classes
[
"font-medium",
size_meta[:text],
variant_meta[:text]
].join(" ")
end
def size_meta
SIZES[@size]
end
def variant_meta
VARIANTS[@variant]
end
attr_reader :variant, :size, :rounded, :full_width, :opts
end

View file

@ -0,0 +1,99 @@
module ButtonStylable
VARIANTS = {
primary: {
container_classes: "text-inverse bg-inverse hover:bg-inverse-hover disabled:bg-gray-500 theme-dark:disabled:bg-gray-400",
icon_classes: "fg-inverse"
},
secondary: {
container_classes: "text-secondary bg-gray-50 theme-dark:bg-gray-700 hover:bg-gray-100 theme-dark:hover:bg-gray-600 disabled:bg-gray-200 theme-dark:disabled:bg-gray-600",
icon_classes: "fg-primary"
},
destructive: {
container_classes: "text-inverse bg-red-500 theme-dark:bg-red-400 hover:bg-red-600 theme-dark:hover:bg-red-500 disabled:bg-red-200 theme-dark:disabled:bg-red-600",
icon_classes: "fg-white"
},
outline: {
container_classes: "text-primary border border-secondary bg-transparent hover:bg-surface-hover",
icon_classes: "fg-gray"
},
outline_destructive: {
container_classes: "text-destructive border border-secondary bg-transparent hover:bg-gray-100 theme-dark:hover:bg-gray-700",
icon_classes: "fg-gray"
},
ghost: {
container_classes: "text-primary bg-transparent hover:bg-gray-100 theme-dark:hover:bg-gray-700",
icon_classes: "fg-gray"
},
icon: {
container_classes: "hover:bg-gray-100 theme-dark:hover:bg-gray-700",
icon_classes: "fg-gray"
},
icon_inverse: {
container_classes: "bg-inverse hover:bg-inverse-hover",
icon_classes: "fg-inverse"
}
}.freeze
SIZES = {
sm: {
padding_classes: "px-2 py-1",
icon_padding_classes: "p-2",
radius_classes: "rounded-md",
text_classes: "text-sm",
icon_classes: "w-4 h-4"
},
md: {
padding_classes: "px-3 py-2",
icon_padding_classes: "p-2",
radius_classes: "rounded-lg",
text_classes: "text-sm",
icon_classes: "w-5 h-5"
},
lg: {
padding_classes: "px-4 py-3",
icon_padding_classes: "p-2",
radius_classes: "rounded-xl",
text_classes: "text-base",
icon_classes: "w-6 h-6"
}
}.freeze
def container_classes
class_names(
"inline-flex items-center gap-1 font-medium whitespace-nowrap",
full_width ? "w-full justify-center" : "",
icon_only? ? SIZES.dig(size, :icon_padding_classes) : SIZES.dig(size, :padding_classes),
rounded ? "rounded-full" : SIZES.dig(size, :radius_classes),
SIZES.dig(size, :text_classes),
VARIANTS.dig(variant, :container_classes)
)
end
def icon_classes
class_names(
SIZES.dig(size, :icon_classes),
VARIANTS.dig(variant, :icon_classes)
)
end
def icon_only?
variant.in?([ :icon, :icon_inverse ])
end
private
def full_width
@full_width ||= false
end
def rounded
@rounded ||= false
end
def variant
@variant ||= :primary
end
def size
@size ||= :md
end
end

View file

@ -1,69 +0,0 @@
# frozen_string_literal: true
class IconComponent < ViewComponent::Base
erb_template <<~ERB
<%= tag.div class: container_classes do %>
<%= lucide_icon(@icon, class: icon_classes) %>
<% end %>
ERB
VARIANTS = {
default: {
icon: "fg-gray",
container: "bg-transparent"
},
destructive: {
icon: "text-destructive",
container: "bg-transparent"
}
}
SIZES = {
sm: {
icon: "w-4 h-4",
container: "w-8 h-8"
},
md: {
icon: "w-5 h-5",
container: "w-10 h-10"
},
lg: {
icon: "w-6 h-6",
container: "w-12 h-12"
}
}
def initialize(icon, variant: "default", size: "md")
@icon = icon
@variant = variant.to_sym
@size = size.to_sym
end
def icon_classes
[
size_meta[:icon],
variant_meta[:icon]
].join(" ")
end
def container_classes
[
"flex justify-center items-center",
show_padding? ? size_meta[:container] : "",
variant_meta[:container]
].join(" ")
end
private
def show_padding?
@variant != :default && @variant != :destructive
end
def variant_meta
VARIANTS[@variant]
end
def size_meta
SIZES[@size]
end
end

View file

@ -0,0 +1,13 @@
<%= link_to href, **merged_opts do %>
<% if icon && (icon_position != "right") %>
<%= lucide_icon(icon, class: icon_classes) %>
<% end %>
<% unless icon_only? %>
<%= text %>
<% end %>
<% if icon && icon_position == "right" %>
<%= lucide_icon(icon, class: icon_classes) %>
<% end %>
<% end %>

View file

@ -0,0 +1,46 @@
class LinkComponent < ViewComponent::Base
include ButtonStylable
attr_reader :href, :variant, :size, :text, :icon, :icon_position, :open_in, :opts
VARIANTS = VARIANTS.merge(
default: {
container_classes: "",
text_classes: "text-primary",
icon_classes: "fg-gray"
},
link_destructive: {
container_classes: "",
text_classes: "text-destructive",
icon_classes: "fg-destructive"
}
).freeze
def initialize(href:, variant: "default", size: "md", text: nil, icon: nil, icon_position: "left", rounded: false, full_width: false, open_in: nil, **opts)
@href = href
@variant = variant.underscore.to_sym
@size = size.underscore.to_sym
@text = text
@icon = icon
@icon_position = icon_position
@rounded = rounded
@full_width = full_width
@open_in = open_in
@opts = opts
end
def merged_opts
merged_opts = opts.dup || {}
extra_classes = merged_opts.delete(:class)
data = merged_opts.delete(:data) || {}
if open_in
data = data.merge(turbo_frame: open_in)
end
merged_opts.merge(
class: class_names(container_classes, extra_classes),
data: data
)
end
end

View file

@ -1,12 +1,12 @@
<%= tag.div data: merged_data do %>
<% if @variant == :icon %>
<%= render ButtonComponent.new(variant: "icon", icon: @icon_vertical ? "more-vertical" : "more-horizontal", data: { menu_target: "button" }) %>
<% elsif @variant == :button %>
<%= tag.div data: { controller: "menu", menu_placement_value: placement, menu_offset_value: offset } do %>
<% if variant == :icon %>
<%= render ButtonComponent.new(variant: "icon", icon: icon_vertical ? "more-vertical" : "more-horizontal", data: { menu_target: "button" }) %>
<% elsif variant == :button %>
<%= button %>
<% elsif @variant == :avatar %>
<% elsif variant == :avatar %>
<button data-menu-target="button">
<div class="w-9 h-9 cursor-pointer">
<%= render "settings/user_avatar", avatar_url: @avatar_url %>
<%= render "settings/user_avatar", avatar_url: avatar_url %>
</div>
</button>
<% end %>

View file

@ -1,8 +1,10 @@
# frozen_string_literal: true
class MenuComponent < ViewComponent::Base
renders_one :button, ->(**options, &block) do
options_with_target = options.merge(data: { menu_target: "button" })
attr_reader :variant, :avatar_url, :placement, :offset, :icon_vertical
renders_one :button, ->(**button_options, &block) do
options_with_target = button_options.merge(data: { menu_target: "button" })
if block
content_tag(:button, **options_with_target, &block)
@ -19,26 +21,15 @@ class MenuComponent < ViewComponent::Base
renders_many :items, MenuItemComponent
VARIANTS = {
icon: {},
button: {},
avatar: {}
}
VARIANTS = %i[icon button avatar].freeze
def initialize(variant: "icon", avatar_url: nil, placement: "bottom-end", offset: 12, icon_vertical: false, data: {})
def initialize(variant: "icon", avatar_url: nil, placement: "bottom-end", offset: 12, icon_vertical: false)
@variant = variant.to_sym
@avatar_url = avatar_url
@placement = placement
@offset = offset
@icon_vertical = icon_vertical
@data = data
end
def merged_data
{
controller: "menu",
menu_placement_value: @placement,
menu_offset_value: @offset
}.merge(@data)
raise ArgumentError, "Invalid variant: #{@variant}" unless VARIANTS.include?(@variant)
end
end

View file

@ -0,0 +1,12 @@
<% if variant == :divider %>
<hr class="border-tertiary my-1">
<% else %>
<div class="px-1">
<%= wrapper do %>
<% if icon %>
<%= lucide_icon(icon, class: destructive? ? "text-destructive" : "fg-gray") %>
<% end %>
<%= tag.span(text, class: text_classes) %>
<% end %>
</div>
<% end %>

View file

@ -1,34 +1,27 @@
class MenuItemComponent < ViewComponent::Base
erb_template <<~ERB
<% if @variant == :divider %>
<hr class="border-tertiary my-1">
<% else %>
<div class="px-1">
<%= wrapper do %>
<% if @icon %>
<%= render IconComponent.new(@icon, variant: destructive? ? "destructive" : "default") %>
<% end %>
<%= tag.span(@text, class: text_classes) %>
<% end %>
</div>
<% end %>
ERB
VARIANTS = %i[link button divider].freeze
def initialize(variant: "default", text: nil, href: nil, method: :get, icon: nil, destructive: false, data: {})
attr_reader :variant, :text, :icon, :href, :method, :destructive, :opts
def initialize(variant:, text: nil, icon: nil, href: nil, method: :post, destructive: false, **opts)
@variant = variant.to_sym
@text = text
@icon = icon
@href = href
@method = method.to_sym
@data = data
@destructive = destructive
@opts = opts
raise ArgumentError, "Invalid variant: #{@variant}" unless VARIANTS.include?(@variant)
end
def wrapper(&block)
if @method.in?([ :post, :patch, :delete ])
button_to @href, method: @method, data: @data, class: container_classes, &block
if variant == :button
button_to href, method: method, class: container_classes, **opts, &block
elsif variant == :link
link_to href, class: container_classes, **opts, &block
else
link_to @href, data: @data, class: container_classes, &block
nil
end
end
@ -40,7 +33,7 @@ class MenuItemComponent < ViewComponent::Base
end
def destructive?
@method == :delete || @destructive
method == :delete || destructive
end
private

View file

@ -1,6 +1,15 @@
module ApplicationHelper
include Pagy::Frontend
def custom_turbo_confirm(title: "Are you sure?", body: "This action cannot be undone.", btn_text: "Confirm", btn_variant: "primary")
{
title: title,
body: body,
confirmText: btn_text,
variant: btn_variant
}
end
def icon(key, size: "md", color: "current")
render partial: "shared/icon", locals: { key:, size:, color: }
end

View file

@ -53,10 +53,13 @@ class StyledFormBuilder < ActionView::Helpers::FormBuilder
value, options = nil, value if value.is_a?(Hash)
value ||= submit_default_value
opts = options.dup
opts[:data] = { turbo_submits_with: "Submitting..." }.merge(opts[:data] || {})
@template.render(ButtonComponent.new(text: value, type: "submit", full_width: true, **opts))
@template.render(
ButtonComponent.new(
text: value,
full_width: true,
data: { turbo_submits_with: "Submitting..." }
)
)
end
private

View file

@ -42,14 +42,14 @@
</div>
<div data-tabs-target="tab" id="assets-tab">
<%= render ButtonComponent.new(
<%= render LinkComponent.new(
text: "New asset",
variant: "ghost",
full_width: true,
left_align: true,
href: new_account_path(step: "method_select", classification: "asset"),
leading_icon: "plus",
data: { turbo_frame: "modal" }
icon: "plus",
open_in: "modal",
full_width: true,
class: "justify-start"
) %>
<div class="space-y-2">
@ -60,14 +60,14 @@
</div>
<div data-tabs-target="tab" id="debts-tab" class="hidden">
<%= render ButtonComponent.new(
<%= render LinkComponent.new(
text: "New debt",
variant: "ghost",
full_width: true,
left_align: true,
href: new_account_path(step: "method_select", classification: "liability"),
leading_icon: "plus",
data: { turbo_frame: "modal" }
icon: "plus",
open_in: "modal",
full_width: true,
class: "justify-start"
) %>
<div class="space-y-2">
@ -78,14 +78,14 @@
</div>
<div data-tabs-target="tab" id="all-tab" class="hidden">
<%= render ButtonComponent.new(
<%= render LinkComponent.new(
text: "New account",
variant: "ghost",
full_width: true,
left_align: true,
href: new_account_path(step: "method_select"),
leading_icon: "plus",
data: { turbo_frame: "modal" }
icon: "plus",
open_in: "modal",
class: "justify-start"
) %>
<div class="space-y-2">

View file

@ -40,13 +40,13 @@
<% end %>
</div>
<%= render ButtonComponent.new(
<%= render LinkComponent.new(
href: new_polymorphic_path(account_group.key, step: "method_select"),
text: "New #{account_group.name.downcase.singularize}",
leading_icon: "plus",
left_align: true,
icon: "plus",
full_width: true,
variant: "ghost",
data: { turbo_frame: "modal" }
open_in: "modal",
class: "justify-start"
) %>
</details>

View file

@ -8,15 +8,15 @@
method: :post,
variant: "outline",
disabled: Current.family.syncing?,
leading_icon: "refresh-cw"
icon: "refresh-cw"
) %>
<%= render ButtonComponent.new(
<%= render LinkComponent.new(
text: "New account",
href: new_account_path(return_to: accounts_path),
variant: "primary",
leading_icon: "plus",
data: { turbo_frame: "modal" }
icon: "plus",
open_in: "modal"
) %>
</div>
</div>

View file

@ -2,20 +2,23 @@
<%= turbo_frame_tag dom_id(account, "entries") do %>
<div class="bg-container p-5 shadow-border-xs rounded-xl" data-controller="focus-record" data-focus-record-id-value="<%= @focused_record ? dom_id(@focused_record) : nil %>">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center justify-between mb-4" data-testid="activity-menu">
<%= tag.h2 t(".title"), class: "font-medium text-lg" %>
<% unless @account.plaid_account_id.present? %>
<%= render MenuComponent.new(variant: "button", data: { testid: "activity-menu" }) do |menu| %>
<% menu.with_button(text: "New", variant: "secondary", leading_icon: "plus") %>
<%= render MenuComponent.new(variant: "button") do |menu| %>
<% menu.with_button(text: "New", variant: "secondary", icon: "plus") %>
<% menu.with_item(
variant: "link",
text: "New balance",
icon: "circle-dollar-sign",
href: new_valuation_path(account_id: @account.id), data: { turbo_frame: :modal }) %>
href: new_valuation_path(account_id: @account.id),
data: { turbo_frame: :modal }) %>
<% unless @account.crypto? %>
<% href = @account.investment? ? new_trade_path(account_id: @account.id) : new_transaction_path(account_id: @account.id) %>
<% menu.with_item(
variant: "link",
text: "New transaction",
icon: "credit-card",
href: href,

View file

@ -1,10 +1,11 @@
<%# locals: (account:) %>
<%= render MenuComponent.new do |menu| %>
<% menu.with_item(text: "Edit", href: edit_account_path(account), icon: "pencil-line", data: { turbo_frame: :modal }) %>
<% menu.with_item(variant: "link", text: "Edit", href: edit_account_path(account), icon: "pencil-line", data: { turbo_frame: :modal }) %>
<% unless account.crypto? %>
<% menu.with_item(
variant: "link",
text: "Import transactions",
href: imports_path({ import: { type: account.investment? ? "TradeImport" : "TransactionImport", account_id: account.id } }),
icon: "download",
@ -13,14 +14,19 @@
<% end %>
<% menu.with_item(
variant: "button",
text: "Delete account",
href: account_path(account),
method: :delete,
icon: "trash-2",
data: { turbo_frame: :_top, turbo_confirm: {
title: t(".confirm_title"),
body: t(".confirm_body_html"),
accept: t(".confirm_accept", name: account.name)
}}
data: {
turbo_frame: :_top,
turbo_confirm: custom_turbo_confirm(
title: t(".confirm_title"),
body: t(".confirm_body_html"),
btn_text: t(".confirm_accept", name: account.name),
btn_variant: "destructive"
)
}
) %>
<% end %>

View file

@ -4,6 +4,7 @@
variant: "primary",
full_width: true,
href: budget_path(budget),
method: :get,
disabled: !budget.allocations_valid?
) %>
</div>

View file

@ -9,15 +9,14 @@
<%= render ButtonComponent.new(
text: "Use defaults (recommended)",
href: bootstrap_categories_path,
method: :post
) %>
<%= render ButtonComponent.new(
<%= render LinkComponent.new(
text: "New category",
variant: "outline",
leading_icon: "plus",
icon: "plus",
href: new_category_path,
data: { turbo_frame: "modal" }
open_in: "modal",
) %>
</div>
</div>

View file

@ -133,7 +133,7 @@
<% end %>
</ul>
<%= render ButtonComponent.new(
<%= render LinkComponent.new(
text: "View all category transactions",
variant: "outline",
full_width: true,
@ -142,7 +142,7 @@
start_date: @budget.start_date,
end_date: @budget.end_date
}),
data: { turbo_frame: :_top }
open_in: :_top
) %>
<% else %>
<p class="text-secondary text-sm mb-4">

View file

@ -12,10 +12,11 @@
<%= format_money(budget.actual_spending_money) %>
</div>
<%= render ButtonComponent.new(
<%= render LinkComponent.new(
text: "of #{budget.budgeted_spending_money.format}",
variant: "secondary",
trailing_icon: "pencil",
icon: "pencil",
icon_position: "right",
size: "sm",
href: edit_budget_path(budget)
) %>
@ -24,10 +25,10 @@
<span><%= format_money Money.new(0, budget.currency || budget.family.currency) %></span>
</div>
<%= render ButtonComponent.new(
<%= render LinkComponent.new(
text: "New budget",
size: "sm",
leading_icon: "plus",
icon: "plus",
href: edit_budget_path(budget)
) %>
<% end %>
@ -45,10 +46,11 @@
<%= format_money(bc.actual_spending_money) %>
</p>
<%= render ButtonComponent.new(
<%= render LinkComponent.new(
text: "of #{bc.budgeted_spending_money.format(precision: 0)}",
variant: "secondary",
trailing_icon: "pencil",
icon: "pencil",
icon_position: "right",
size: "sm",
href: budget_budget_categories_path(budget)
) %>

View file

@ -31,7 +31,7 @@
</div>
<div class="ml-auto">
<%= render ButtonComponent.new(
<%= render LinkComponent.new(
text: "Today",
variant: "outline",
href: budget_path(Budget.date_to_param(Date.current)),

View file

@ -4,11 +4,12 @@
<%= lucide_icon "alert-triangle", class: "w-6 h-6 text-red-500" %>
<p class="text-secondary text-sm text-center">You have over-allocated your budget. Please fix your allocations.</p>
<%= render ButtonComponent.new(
<%= render LinkComponent.new(
text: "Fix allocations",
variant: "secondary",
size: "sm",
trailing_icon: "pencil",
icon: "pencil",
icon_position: "right",
href: budget_budget_categories_path(budget)
) %>
</div>

View file

@ -38,7 +38,13 @@
<% param_key = Budget.date_to_param(date) %>
<% if Budget.budget_date_valid?(date, family: family) %>
<%= render ButtonComponent.new(variant: "ghost", text: month_name, href: budget_path(param_key), data: { turbo_frame: :_top }) %>
<%= render LinkComponent.new(
variant: "ghost",
text: month_name,
href: budget_path(param_key),
full_width: true,
open_in: :_top
) %>
<% else %>
<span class="px-3 py-2 text-subdued rounded-md"><%= month_name %></span>
<% end %>

View file

@ -54,10 +54,10 @@
<h2 class="text-lg font-medium">Categories</h2>
<% if @budget.initialized? %>
<%= render ButtonComponent.new(
<%= render LinkComponent.new(
text: "Edit",
variant: "secondary",
leading_icon: "settings-2",
icon: "settings-2",
href: budget_budget_categories_path(@budget)
) %>
<% end %>

View file

@ -11,12 +11,12 @@
<div class="justify-self-end">
<%= render MenuComponent.new do |menu| %>
<% menu.with_item(text: t(".edit"), icon: "pencil", href: edit_category_path(category), data: { turbo_frame: :modal }) %>
<% menu.with_item(variant: "link", text: t(".edit"), icon: "pencil", href: edit_category_path(category), data: { turbo_frame: :modal }) %>
<% if category.transactions.any? %>
<% menu.with_item(text: t(".delete"), icon: "trash-2", href: new_category_deletion_path(category), data: { turbo_frame: :modal }, method: :delete) %>
<% menu.with_item(variant: "link", text: t(".delete"), icon: "trash-2", href: new_category_deletion_path(category), data: { turbo_frame: :modal }) %>
<% else %>
<% menu.with_item(text: t(".delete"), icon: "trash-2", href: category_path(category), method: :delete) %>
<% menu.with_item(variant: "link", text: t(".delete"), icon: "trash-2", href: category_path(category), method: :delete) %>
<% end %>
<% end %>
</div>

View file

@ -4,26 +4,28 @@
<div class="flex items-center gap-2">
<%= render MenuComponent.new do |menu| %>
<% menu.with_item(
variant: "button",
text: "Delete all",
href: destroy_all_categories_path,
method: :delete,
icon: "trash-2",
data: {
turbo_confirm: {
turbo_confirm: custom_turbo_confirm(
title: "Delete all categories?",
body: "All of your transactions will become uncategorized and this cannot be undone.",
accept: "Delete all categories",
}
btn_text: "Delete all categories",
btn_variant: "destructive"
)
}
) %>
<% end %>
<%= render ButtonComponent.new(
<%= render LinkComponent.new(
text: t(".new"),
variant: "primary",
leading_icon: "plus",
icon: "plus",
href: new_category_path,
data: { turbo_frame: :modal }
open_in: :modal
) %>
</div>
</header>
@ -47,15 +49,14 @@
<%= render ButtonComponent.new(
text: t(".bootstrap"),
href: bootstrap_categories_path,
method: :post
) %>
<%= render ButtonComponent.new(
<%= render LinkComponent.new(
text: t(".new"),
variant: "outline",
leading_icon: "plus",
icon: "plus",
href: new_category_path,
data: { turbo_frame: :modal }
open_in: :modal
) %>
</div>
</div>

View file

@ -25,7 +25,7 @@
<% end %>
<%= render MenuComponent.new do |menu| %>
<% menu.with_item(text: t(".edit"), icon: "pencil-line", href: edit_category_path(category), data: { turbo_frame: :modal }) %>
<% menu.with_item(text: t(".delete"), icon: "trash-2", href: new_category_deletion_path(category), data: { turbo_frame: :modal }, destructive: true) %>
<% menu.with_item(variant: "link", text: t(".edit"), icon: "pencil-line", href: edit_category_path(category), data: { turbo_frame: :modal }) %>
<% menu.with_item(variant: "link", text: t(".delete"), icon: "trash-2", href: new_category_deletion_path(category), data: { turbo_frame: :modal }, destructive: true) %>
<% end %>
<% end %>

View file

@ -10,17 +10,20 @@
</div>
<%= render MenuComponent.new(icon_vertical: true) do |menu| %>
<% menu.with_item(text: "Edit chat", href: edit_chat_path(chat), icon: "pencil", data: { turbo_frame: dom_id(chat, "title") }) %>
<% menu.with_item(variant: "link", text: "Edit chat", href: edit_chat_path(chat), icon: "pencil", open_in: dom_id(chat, "title")) %>
<% menu.with_item(
variant: "button",
text: "Delete chat",
href: chat_path(chat),
icon: "trash-2",
method: :delete,
data: {
turbo_confirm: {
title: "Are you sure you want to delete this chat?",
variant: "outline-destructive"
}
turbo_confirm: custom_turbo_confirm(
title: "Delete chat?",
body: "Are you sure you want to delete this chat? This action cannot be undone.",
btn_text: "Delete chat",
btn_variant: "outline-destructive"
)
}) %>
<% end %>
<% end %>

View file

@ -14,20 +14,27 @@
</div>
<%= render MenuComponent.new(icon_vertical: true) do |menu| %>
<% menu.with_item(text: "Start new chat", href: new_chat_path, icon: "plus") %>
<% menu.with_item(variant: "link", text: "Start new chat", href: new_chat_path, icon: "plus") %>
<% unless chat.new_record? %>
<% menu.with_item(text: "Edit chat title", href: edit_chat_path(chat, ctx: "chat"), icon: "pencil", data: { turbo_frame: dom_id(chat, "title") }) %>
<% menu.with_item(
variant: "link",
text: "Edit chat title",
href: edit_chat_path(chat, ctx: "chat"),
icon: "pencil", data: { turbo_frame: dom_id(chat, "title") }) %>
<% menu.with_item(
variant: "button",
text: "Delete chat",
href: chat_path(chat),
icon: "trash-2",
method: :delete,
data: {
turbo_confirm: {
turbo_confirm: custom_turbo_confirm(
title: "Are you sure you want to delete this chat?",
variant: "outline-destructive"
}
btn_text: "Delete chat",
btn_variant: "outline-destructive"
)
}) %>
<% end %>
<% end %>

View file

@ -12,9 +12,7 @@
<%= render ButtonComponent.new(
text: "Retry",
variant: "primary",
href: retry_chat_path(chat),
method: :post
) %>
</div>
</div>

View file

@ -27,10 +27,10 @@
</div>
<div class="flex justify-center py-8">
<%= render ButtonComponent.new(
<%= render LinkComponent.new(
text: "Edit account details",
variant: "ghost",
href: edit_credit_card_path(account),
data: { turbo_frame: :modal }
open_in: :modal
) %>
</div>

View file

@ -16,12 +16,21 @@
</div>
<div class="justify-self-end">
<%= render MenuComponent.new do |menu| %>
<% menu.with_item(text: "Edit", href: edit_family_merchant_path(family_merchant), icon: "pencil", data: { turbo_frame: "modal" }) %>
<% menu.with_item(text: "Delete", href: family_merchant_path(family_merchant), icon: "trash-2", method: :delete, data: { turbo_confirm: {
title: "Delete #{family_merchant.name}?",
body: "This will remove this merchant from all transactions it has been assigned to.",
variant: "outline-destructive"
} }) %>
<% menu.with_item(variant: "link", text: "Edit", href: edit_family_merchant_path(family_merchant), icon: "pencil", data: { turbo_frame: "modal" }) %>
<% menu.with_item(
variant: "button",
text: "Delete",
href: family_merchant_path(family_merchant),
icon: "trash-2",
method: :delete,
data: {
turbo_confirm: custom_turbo_confirm(
title: "Delete #{family_merchant.name}?",
body: "This will remove this merchant from all transactions it has been assigned to.",
btn_text: "Delete #{family_merchant.name}",
btn_variant: "outline-destructive"
)
}) %>
<% end %>
</div>
</div>

View file

@ -1,10 +1,11 @@
<header class="flex items-center justify-between">
<h1 class="text-primary text-xl font-medium">Merchants</h1>
<%= render ButtonComponent.new(
<%= render LinkComponent.new(
text: "New merchant",
variant: "primary",
href: new_family_merchant_path,
data: { turbo_frame: "modal" }
open_in: :modal
) %>
</header>

View file

@ -17,10 +17,11 @@
<p class="text-green-500 text-sm md:text-base">Your data has been cleaned</p>
</div>
<%= render ButtonComponent.new(
<%= render LinkComponent.new(
text: "Next step",
variant: "primary",
href: import_confirm_path(@import),
data: { turbo_frame: :_top },
open_in: :_top,
class: "w-full md:w-auto"
) %>
</div>

View file

@ -18,7 +18,7 @@
<p class="text-sm text-secondary">We found a configuration from a previous import for this account. Would you like to apply it to this import?</p>
<div class="mt-4 flex gap-2 items-center">
<%= render ButtonComponent.new(text: "Manually configure", href: import_configuration_path(@import), variant: "outline") %>
<%= render LinkComponent.new(text: "Manually configure", href: import_configuration_path(@import), variant: "outline") %>
<%= render ButtonComponent.new(text: "Apply template", href: apply_template_import_path(@import), method: :put, data: { turbo_frame: :_top }) %>
</div>
</div>

View file

@ -11,7 +11,12 @@
<div class="flex items-center justify-between p-4 gap-4 text-secondary bg-red-100 border border-red-200 rounded-lg w-[650px] min-w-0 mx-auto">
<%= tag.p t(".no_accounts"), class: "text-sm" %>
<%= render ButtonComponent.new(text: "Create account", href: new_account_path(return_to: import_confirm_path(import)), data: { turbo_frame: :modal }) %>
<%= render LinkComponent.new(
text: "Create account",
variant: "primary",
href: new_account_path(return_to: import_confirm_path(import)),
open_in: :modal
) %>
</div>
</div>
</div>
@ -20,7 +25,12 @@
<div class="overflow-x-auto">
<div class="flex items-center justify-between p-4 gap-4 text-secondary bg-yellow-100 border border-yellow-200 rounded-lg w-[650px] min-w-0 mx-auto">
<%= tag.p t(".unassigned_account"), class: "text-sm" %>
<%= render ButtonComponent.new(text: t(".create_account"), href: new_account_path(return_to: import_confirm_path(import)), data: { turbo_frame: :modal }) %>
<%= render LinkComponent.new(
text: t(".create_account"),
variant: "primary",
href: new_account_path(return_to: import_confirm_path(import)),
open_in: :modal
) %>
</div>
</div>
</div>
@ -49,7 +59,14 @@
</div>
<div class="flex justify-center w-full">
<%= render ButtonComponent.new(text: "Next", href: is_last_step ? import_path(import) : url_for(step: step_idx + 2), trailing_icon: "arrow-right", class: "w-full md:w-auto") %>
<%= render LinkComponent.new(
text: "Next",
variant: "primary",
href: is_last_step ? import_path(import) : url_for(step: step_idx + 2),
icon: "arrow-right",
icon_position: "right",
class: "w-full md:w-auto"
) %>
</div>
</div>
</div>

View file

@ -2,6 +2,12 @@
<div class="text-center flex flex-col items-center max-w-[300px] gap-4">
<p class="text-primary mb-1 font-medium text-sm"><%= t(".message") %></p>
<%= render ButtonComponent.new(text: t(".new"), href: new_import_path, leading_icon: "plus", data: { turbo_frame: "modal" }) %>
<%= render LinkComponent.new(
text: t(".new"),
variant: "primary",
href: new_import_path,
icon: "plus",
open_in: :modal
) %>
</div>
</div>

View file

@ -11,6 +11,6 @@
<p class="text-sm text-secondary">Please check that your file format, for any errors and that all required fields are filled, then come back and try again.</p>
</div>
<%= render ButtonComponent.new(text: "Try again", href: publish_import_path(import), method: :post) %>
<%= render ButtonComponent.new(text: "Try again", href: publish_import_path(import), full_width: true) %>
</div>
</div>

View file

@ -37,24 +37,27 @@
</div>
<%= render MenuComponent.new do |menu| %>
<% menu.with_item(text: t(".view"), href: import_path(import), icon: "eye") %>
<% menu.with_item(variant: "link", text: t(".view"), href: import_path(import), icon: "eye") %>
<% if import.complete? || import.revert_failed? %>
<% menu.with_item(
variant: "button",
text: t(".revert"),
href: revert_import_path(import),
icon: "rotate-ccw",
method: :put,
data: {
turbo_confirm: {
turbo_confirm: custom_turbo_confirm(
title: "Revert import?",
body: "This will delete transactions that were imported, but you will still be able to review and re-import your data at any time.",
accept: "Revert"
}
btn_text: "Revert",
btn_variant: "outline-destructive"
)
}) %>
<% else %>
<% menu.with_item(
variant: "button",
text: t(".delete"),
href: import_path(import),
icon: "trash-2",
@ -63,7 +66,8 @@
turbo_confirm: {
title: "Delete import?",
body: "This will delete the import and is not reversible.",
variant: "outline-destructive"
btn_text: "Delete import",
btn_variant: "outline-destructive"
}
}) %>
<% end %>

View file

@ -12,8 +12,8 @@
</div>
<div class="space-y-2">
<%= render ButtonComponent.new(text: "Check status", href: import_path(import)) %>
<%= render ButtonComponent.new(text: "Back to dashboard", href: root_path, variant: "secondary") %>
<%= render LinkComponent.new(text: "Check status", href: import_path(import), variant: "primary") %>
<%= render LinkComponent.new(text: "Back to dashboard", href: root_path, variant: "secondary") %>
</div>
</div>
</div>

View file

@ -35,5 +35,5 @@
</div>
</div>
<%= render ButtonComponent.new(text: "Publish import", href: publish_import_path(import), method: :post) %>
<%= render ButtonComponent.new(text: "Publish import", href: publish_import_path(import)) %>
</div>

View file

@ -14,8 +14,7 @@
<%= render ButtonComponent.new(
text: "Try again",
full_width: true,
href: revert_import_path(import),
method: :post
href: revert_import_path(import)
) %>
</div>
</div>

View file

@ -11,8 +11,9 @@
<p class="text-sm text-secondary">Your imported data has been successfully added to the app and is now ready for use.</p>
</div>
<%= render ButtonComponent.new(
<%= render LinkComponent.new(
text: "Back to dashboard",
variant: "primary",
full_width: true,
href: root_path
) %>

View file

@ -1,11 +1,12 @@
<div class="flex items-center justify-between">
<h1 class="text-xl font-medium text-primary"><%= t(".title") %></h1>
<%= render ButtonComponent.new(
<%= render LinkComponent.new(
text: "New import",
href: new_import_path,
leading_icon: "plus",
data: { turbo_frame: :modal }
icon: "plus",
variant: "primary",
open_in: :modal
) %>
</div>

View file

@ -8,7 +8,7 @@
<%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %>
<%= javascript_importmap_tags %>
</head>
<body class="p-4 bg-surface">
<body class="p-4 bg-container">
<%= yield %>
</body>
</html>

View file

@ -8,7 +8,6 @@
variant: "icon",
icon: "x",
value: "cancel",
type: "submit"
) %>
</div>
@ -18,15 +17,14 @@
<div>
<% ["primary", "outline-destructive", "destructive"].each do |variant| %>
<%= render ButtonComponent.new(
text: "Confirm",
variant: variant,
autofocus: true,
full_width: true,
value: "confirm",
data: { variant: variant, confirm_dialog_target: "confirmButton" },
hidden: true,
type: "submit"
) %>
text: "Confirm",
variant: variant,
autofocus: true,
full_width: true,
value: "confirm",
data: { variant: variant, confirm_dialog_target: "confirmButton" },
hidden: true,
) %>
<% end %>
</div>
</form>

View file

@ -45,10 +45,10 @@
</div>
<div class="flex justify-center py-8">
<%= render ButtonComponent.new(
<%= render LinkComponent.new(
text: "Edit loan details",
variant: "ghost",
href: edit_loan_path(account),
data: { turbo_frame: :modal }
open_in: :modal
) %>
</div>

View file

@ -16,18 +16,20 @@
</div>
<div class="justify-self-end">
<%= render MenuComponent.new do |menu| %>
<% menu.with_item(text: t(".edit"), href: edit_merchant_path(merchant), icon: "pencil", data: { turbo_frame: "modal" }) %>
<% menu.with_item(variant: "link", text: t(".edit"), href: edit_merchant_path(merchant), icon: "pencil", data: { turbo_frame: "modal" }) %>
<% menu.with_item(
variant: "button",
text: t(".delete"),
href: merchant_path(merchant),
icon: "trash-2",
method: :delete,
data: { turbo_confirm: {
data: { turbo_confirm: custom_turbo_confirm(
title: t(".confirm_title"),
body: t(".confirm_body"),
confirmText: t(".confirm_accept"),
variant: "outline-destructive"
} }) %>
btn_text: t(".confirm_accept"),
btn_variant: "outline-destructive"
)
}) %>
<% end %>
</div>
</div>

View file

@ -19,9 +19,10 @@
<% end %>
</div>
<%= render ButtonComponent.new(
<%= render LinkComponent.new(
text: t(".continue"),
href: settings_security_path,
variant: "primary",
full_width: true
) %>
</div>

View file

@ -5,9 +5,10 @@
<%= tag.h1 t(".title"), class: "text-3xl font-medium mb-2" %>
<%= tag.p t(".message"), class: "text-sm text-secondary mb-6" %>
<%= render ButtonComponent.new(
<%= render LinkComponent.new(
text: t(".setup"),
href: profile_onboarding_path,
variant: "primary",
full_width: true
) %>
</div>

View file

@ -5,12 +5,11 @@
<p class="text-gray-500">Here's what's happening with your finances</p>
</div>
<%= render ButtonComponent.new(
<%= render LinkComponent.new(
variant: "icon-inverse",
icon: "plus",
href: new_account_path(step: "method_select", classification: "asset"),
data: { turbo_frame: "modal" },
class: "rounded-full! md:hidden"
open_in: :modal
) %>
</div>
<% end %>

View file

@ -7,11 +7,11 @@
<p class="text-secondary"><%= t(".no_account_subtitle") %></p>
</div>
<%= render ButtonComponent.new(
<%= render LinkComponent.new(
text: t(".new_account"),
href: new_account_path,
leading_icon: "plus",
data: { turbo_frame: "modal" }
icon: "plus",
open_in: :modal
) %>
</div>
</div>

View file

@ -53,7 +53,7 @@
<%= render ButtonComponent.new(
text: t(".update"),
leading_icon: "refresh-cw",
icon: "refresh-cw",
variant: "secondary",
data: {
controller: "plaid",
@ -76,21 +76,23 @@
<div class="flex items-center gap-2">
<%= render ButtonComponent.new(
text: t(".delete"),
leading_icon: "trash-2",
icon: "trash-2",
variant: "destructive",
href: plaid_item_path(plaid_item),
method: :delete,
data: {
turbo_confirm: {
turbo_confirm: custom_turbo_confirm(
title: t(".confirm_title"),
body: t(".confirm_body"),
accept: t(".confirm_accept")
}
btn_text: t(".confirm_accept"),
btn_variant: "destructive"
)
}
) %>
<%= render ButtonComponent.new(
<%= render LinkComponent.new(
text: t(".add_new"),
leading_icon: "plus",
icon: "plus",
variant: "secondary",
href: new_account_path
) %>
@ -102,18 +104,23 @@
variant: "icon",
icon: "refresh-cw",
href: sync_plaid_item_path(plaid_item),
method: :post,
disabled: plaid_item.syncing? || plaid_item.scheduled_for_deletion?
) %>
<% end %>
<%= render MenuComponent.new do |menu| %>
<% menu.with_item text: t(".delete"), icon: "trash-2", href: plaid_item_path(plaid_item), method: :delete, data: { turbo_confirm: {
title: t(".confirm_title"),
body: t(".confirm_body"),
accept: t(".confirm_accept"),
variant: "destructive"
} } %>
<% menu.with_item(
variant: "button",
text: t(".delete"),
icon: "trash-2",
href: plaid_item_path(plaid_item),
method: :delete,
data: { turbo_confirm: custom_turbo_confirm(
title: t(".confirm_title"),
body: t(".confirm_body"),
btn_text: t(".confirm_accept"),
btn_variant: "destructive") }
) %>
<% end %>
</div>
</summary>

View file

@ -29,10 +29,10 @@
</div>
<div class="flex justify-center py-8">
<%= render ButtonComponent.new(
<%= render LinkComponent.new(
text: "Edit account details",
href: edit_property_path(account),
variant: "ghost",
data: { turbo_frame: :modal }
open_in: :modal
) %>
</div>

View file

@ -13,9 +13,9 @@
<%= f.hidden_field :rule_prompt_dismissed_at, value: Time.current %>
<%= tag.div class:"flex gap-2 justify-end" do %>
<%= render ButtonComponent.new(text: "Dismiss", variant: "secondary", type: "submit") %>
<%= render ButtonComponent.new(text: "Dismiss", variant: "secondary") %>
<% rule_href = new_rule_path(resource_type: "transaction", action_type: "set_transaction_category", action_value: cta[:category_id]) %>
<%= render ButtonComponent.new(text: "Create rule", href: rule_href, data: { turbo_frame: :modal }) %>
<%= render LinkComponent.new(text: "Create rule", variant: "primary", href: rule_href, open_in: :modal) %>
<% end %>
<% end %>
<% end %>

View file

@ -37,8 +37,8 @@
</ul>
<div class="flex items-center gap-2">
<%= render ButtonComponent.new(text: "Add condition", leading_icon: "plus", variant: "ghost", data: { action: "rules#addCondition" }) %>
<%= render ButtonComponent.new(text: "Add condition group", leading_icon: "boxes", variant: "ghost", data: { action: "rules#addConditionGroup" }) %>
<%= render ButtonComponent.new(text: "Add condition", icon: "plus", variant: "ghost", data: { action: "rules#addCondition" }) %>
<%= render ButtonComponent.new(text: "Add condition group", icon: "boxes", variant: "ghost", data: { action: "rules#addConditionGroup" }) %>
</div>
</section>
@ -58,7 +58,7 @@
<% end %>
</ul>
<%= render ButtonComponent.new(text: "Add action", leading_icon: "plus", variant: "ghost", data: { action: "rules#addAction" }) %>
<%= render ButtonComponent.new(text: "Add action", icon: "plus", variant: "ghost", data: { action: "rules#addAction" }) %>
</section>
<section class="space-y-4">

View file

@ -45,14 +45,22 @@
<%= render "shared/toggle_form", model: rule, attribute: :active %>
<%= render MenuComponent.new do |menu| %>
<% menu.with_item(text: "Edit", href: edit_rule_path(rule), icon: "pencil", data: { turbo_frame: "modal" }) %>
<% menu.with_item(text: "Re-apply rule", href: confirm_rule_path(rule), icon: "refresh-cw", data: { turbo_frame: "modal" }) %>
<% menu.with_item(text: "Delete", href: rule_path(rule), icon: "trash-2", method: :delete, data: { turbo_confirm: {
title: "Delete rule",
body: "Are you sure you want to delete this rule? Data affected by this rule will no longer be automatically updated. This action cannot be undone.",
confirmText: "Delete rule",
variant: "outline-destructive"
} }) %>
<% menu.with_item(variant: "link", text: "Edit", href: edit_rule_path(rule), icon: "pencil", data: { turbo_frame: "modal" }) %>
<% menu.with_item(variant: "link", text: "Re-apply rule", href: confirm_rule_path(rule), icon: "refresh-cw", data: { turbo_frame: "modal" }) %>
<% menu.with_item(
variant: "button",
text: "Delete",
href: rule_path(rule),
icon: "trash-2",
method: :delete,
data: {
turbo_confirm: custom_turbo_confirm(
title: "Delete rule?",
body: "Are you sure you want to delete this rule? Data affected by this rule will no longer be automatically updated. This action cannot be undone.",
btn_text: "Delete rule",
btn_variant: "outline-destructive"
)
}) %>
<% end %>
</div>
</div>

View file

@ -5,22 +5,29 @@
<% if @rules.any? %>
<%= render MenuComponent.new do |menu| %>
<% menu.with_item(
variant: "button",
text: "Delete all rules",
href: destroy_all_rules_path,
icon: "trash-2",
method: :delete,
data: {
turbo_confirm: {
title: "Delete all rules",
turbo_confirm: custom_turbo_confirm(
title: "Delete all rules?",
body: "Are you sure you want to delete all rules? This action cannot be undone.",
confirmText: "Delete all rules",
variant: "destructive"
}
btn_text: "Delete all rules",
btn_variant: "destructive"
)
}) %>
<% end %>
<% end %>
<%= render ButtonComponent.new(text: "New rule", href: new_rule_path(resource_type: "transaction"), leading_icon: "plus", data: { turbo_frame: :modal }) %>
<%= render LinkComponent.new(
text: "New rule",
variant: "primary",
href: new_rule_path(resource_type: "transaction"),
icon: "plus",
open_in: :modal
) %>
</div>
</header>
@ -53,7 +60,13 @@
<p class="text-sm text-primary font-medium mb-1">No rules yet</p>
<p class="text-sm text-secondary mb-4">Set up rules to perform actions to your transactions and other data on every account sync.</p>
<div class="flex items-center gap-2">
<%= render ButtonComponent.new(text: "New rule", href: new_rule_path(resource_type: "transaction"), leading_icon: "plus", data: { turbo_frame: :modal }) %>
<%= render LinkComponent.new(
text: "New rule",
variant: "primary",
href: new_rule_path(resource_type: "transaction"),
icon: "plus",
open_in: :modal
) %>
</div>
</div>
</div>

View file

@ -19,9 +19,24 @@
</div>
<% if @user.family.subscribed? || subscription_pending? %>
<%= render ButtonComponent.new(text: "Manage", trailing_icon: "external-link", href: subscription_path, target: "_blank", rel: "noopener") %>
<%= render LinkComponent.new(
text: "Manage",
icon: "external-link",
variant: "primary",
icon_position: "right",
href: subscription_path,
target: "_blank",
rel: "noopener"
) %>
<% else %>
<%= render ButtonComponent.new(text: "Subscribe", trailing_icon: "external-link", href: new_subscription_path, target: "_blank", rel: "noopener") %>
<%= render LinkComponent.new(
text: "Subscribe",
variant: "primary",
icon: "external-link",
icon_position: "right",
href: new_subscription_path,
target: "_blank",
rel: "noopener") %>
<% end %>
</div>

View file

@ -57,12 +57,12 @@
href: settings_profile_path(user_id: user),
method: :delete,
data: {
turbo_confirm: {
turbo_confirm: custom_turbo_confirm(
title: t(".confirm_remove_member.title"),
body: t(".confirm_remove_member.body", name: user.display_name),
confirmText: t(".remove_member"),
variant: "destructive"
}
btn_text: t(".remove_member"),
btn_variant: "destructive"
)
}
) %>
</div>
@ -111,12 +111,12 @@
href: invitation_path(invitation),
method: :delete,
data: {
turbo_confirm: {
turbo_confirm: custom_turbo_confirm(
title: t(".confirm_remove_invitation.title"),
body: t(".confirm_remove_invitation.body", email: invitation.email),
confirmText: t(".remove_invitation"),
variant: "outline-destructive"
}
btn_text: t(".remove_invitation"),
btn_variant: "outline-destructive"
)
}
) %>
<% end %>
@ -151,12 +151,12 @@
href: reset_user_path(@user),
method: :delete,
data: {
turbo_confirm: {
turbo_confirm: custom_turbo_confirm(
title: t(".confirm_reset.title"),
body: t(".confirm_reset.body"),
confirmText: t(".reset_account"),
variant: "destructive"
}
btn_text: t(".reset_account"),
btn_variant: "destructive"
)
}
) %>
</div>
@ -173,12 +173,12 @@
href: user_path(@user),
method: :delete,
data: {
turbo_confirm: {
turbo_confirm: custom_turbo_confirm(
title: t(".confirm_delete.title"),
body: t(".confirm_delete.body"),
confirmText: t(".delete_account"),
variant: "destructive"
}
btn_text: t(".delete_account"),
btn_variant: "destructive"
)
}
) %>
</div>

View file

@ -27,18 +27,19 @@
href: disable_mfa_path,
method: :delete,
data: {
turbo_confirm: {
turbo_confirm: custom_turbo_confirm(
title: t(".disable_mfa_confirm"),
body: t(".disable_mfa_confirm"),
confirmText: t(".disable_mfa"),
variant: "outline-destructive"
}
btn_text: t(".disable_mfa"),
btn_variant: "outline-destructive"
)
}
) %>
<% else %>
<%= render ButtonComponent.new(
text: t(".enable_mfa"),
href: new_mfa_path
<%= render LinkComponent.new(
text: t(".enable_mfa"),
variant: "primary",
href: new_mfa_path
) %>
<% end %>
</div>

View file

@ -16,10 +16,11 @@
<p>To continue using the app, please subscribe. In this early beta testing phase, we require that you upgrade within one hour to claim your spot.</p>
</div>
<%= render ButtonComponent.new(
<%= render LinkComponent.new(
text: "Upgrade to Maybe+",
href: new_subscription_path,
class: "w-full"
variant: "primary",
full_width: true
) %>
</div>
</div>

View file

@ -9,14 +9,20 @@
</div>
<div class="justify-self-end">
<%= render MenuComponent.new do |menu| %>
<% menu.with_item(text: t(".edit"), href: edit_tag_path(tag), icon: "pencil", data: { turbo_frame: "modal" }) %>
<% menu.with_item(variant: "link", text: t(".edit"), href: edit_tag_path(tag), icon: "pencil", data: { turbo_frame: "modal" }) %>
<% menu.with_item(text: t(".delete"), href: tag_path(tag), icon: "trash-2", method: :delete, data: { turbo_confirm: {
title: "Delete tag",
<% menu.with_item(
variant: "button",
text: t(".delete"),
href: tag_path(tag),
icon: "trash-2",
method: :delete,
data: { turbo_confirm: custom_turbo_confirm(
title: "Delete tag?",
body: "Are you sure you want to delete this tag and remove it from assigned transactions? This action cannot be undone.",
confirmText: "Delete tag",
variant: "outline-destructive"
} }) %>
btn_text: "Delete tag",
btn_variant: "outline-destructive"
) }) %>
<% end %>
</div>
</div>

View file

@ -1,12 +1,12 @@
<header class="flex items-center justify-between">
<h1 class="text-primary text-xl font-medium"><%= t(".tags") %></h1>
<%= render ButtonComponent.new(
<%= render LinkComponent.new(
text: t(".new"),
variant: "primary",
href: new_tag_path,
leading_icon: "plus",
data: { turbo_frame: :modal }
icon: "plus",
open_in: :modal
) %>
</header>

View file

@ -50,7 +50,7 @@
</div>
<div class="flex justify-end items-center gap-2">
<%= render ButtonComponent.new(
<%= render LinkComponent.new(
text: "Cancel",
variant: "ghost",
href: transactions_path

View file

@ -4,40 +4,45 @@
<div class="flex items-center gap-5">
<div class="flex items-center gap-2">
<%= render MenuComponent.new do |menu| %>
<% menu.with_item(text: "Dev only: Sync all", href: sync_all_accounts_path, method: :post, icon: "refresh-cw") %>
<% menu.with_item(text: "New rule", href: new_rule_path(resource_type: "transaction"), icon: "plus", data: { turbo_frame: :modal }) %>
<% menu.with_item(text: "Edit rules", href: rules_path, icon: "git-branch", data: { turbo_frame: :_top }) %>
<% menu.with_item(text: "Edit categories", href: categories_path, icon: "shapes", data: { turbo_frame: :_top }) %>
<% menu.with_item(text: "Edit tags", href: tags_path, icon: "tags", data: { turbo_frame: :_top }) %>
<% menu.with_item(text: "Edit merchants", href: family_merchants_path, icon: "store", data: { turbo_frame: :_top }) %>
<% menu.with_item(text: "Edit imports", href: imports_path, icon: "hard-drive-upload", data: { turbo_frame: :_top }) %>
<% menu.with_item(text: "Import", href: new_import_path, icon: "download", data: { turbo_frame: "modal", class_name: "md:!hidden" }) %>
<% menu.with_item(variant: "button", text: "Dev only: Sync all", href: sync_all_accounts_path, method: :post, icon: "refresh-cw") %>
<% menu.with_item(variant: "link", text: "New rule", href: new_rule_path(resource_type: "transaction"), icon: "plus", data: { turbo_frame: :modal }) %>
<% menu.with_item(variant: "link", text: "Edit rules", href: rules_path, icon: "git-branch", data: { turbo_frame: :_top }) %>
<% menu.with_item(variant: "link", text: "Edit categories", href: categories_path, icon: "shapes", data: { turbo_frame: :_top }) %>
<% menu.with_item(variant: "link", text: "Edit tags", href: tags_path, icon: "tags", data: { turbo_frame: :_top }) %>
<% menu.with_item(variant: "link", text: "Edit merchants", href: family_merchants_path, icon: "store", data: { turbo_frame: :_top }) %>
<% menu.with_item(variant: "link", text: "Edit imports", href: imports_path, icon: "hard-drive-upload", data: { turbo_frame: :_top }) %>
<% menu.with_item(variant: "link", text: "Import", href: new_import_path, icon: "download", data: { turbo_frame: "modal", class_name: "md:!hidden" }) %>
<% end %>
<%= render ButtonComponent.new(
text: t(".import"),
leading_icon: "download",
variant: "outline",
href: new_import_path,
data: { turbo_frame: :modal },
class: "hidden md:flex"
) %>
<div class="hidden md:flex">
<%= render LinkComponent.new(
text: t(".import"),
icon: "download",
variant: "outline",
href: new_import_path,
open_in: :modal,
) %>
</div>
<%= render ButtonComponent.new(
text: "New transaction",
leading_icon: "plus",
href: new_transaction_path,
data: { turbo_frame: :modal },
class: "hidden md:flex"
) %>
<div class="hidden md:flex">
<%= render LinkComponent.new(
text: "New transaction",
icon: "plus",
variant: "primary",
href: new_transaction_path,
open_in: :modal,
) %>
</div>
<%= render ButtonComponent.new(
icon: "plus",
variant: "icon-inverse",
href: new_transaction_path,
data: { turbo_frame: :modal },
class: "md:hidden !rounded-full"
) %>
<div class="md:hidden">
<%= render LinkComponent.new(
icon: "plus",
variant: "icon-inverse",
href: new_transaction_path,
open_in: :modal,
rounded: true
) %>
</div>
</div>
</div>
</header>

View file

@ -19,9 +19,10 @@
<div data-controller="menu" class="relative">
<%= render ButtonComponent.new(
text: "Filter",
leading_icon: "list-filter",
icon: "list-filter",
variant: "outline",
id: "transaction-filters-button",
type: "button",
data: { menu_target: "button" }
) %>

View file

@ -32,7 +32,7 @@
<div class="flex justify-between items-center gap-2 bg-container p-3">
<div>
<% if @q.present? %>
<%= render ButtonComponent.new(
<%= render LinkComponent.new(
text: t(".clear_filters"),
variant: "ghost",
href: transactions_path(clear_filters: true),
@ -41,8 +41,8 @@
</div>
<div>
<%= render ButtonComponent.new(text: t(".cancel"), variant: "ghost", data: { action: "menu#close" }) %>
<%= render ButtonComponent.new(text: t(".apply"), type: "submit") %>
<%= render ButtonComponent.new(text: t(".cancel"), type: "button", variant: "ghost", data: { action: "menu#close" }) %>
<%= render ButtonComponent.new(text: t(".apply")) %>
</div>
</div>
</div>

View file

@ -125,12 +125,12 @@
<p class="text-secondary">Transfers and payments are special types of transactions that indicate money movement between 2 accounts.</p>
</div>
<%= render ButtonComponent.new(
<%= render LinkComponent.new(
text: "Open matcher",
leading_icon: "arrow-left-right",
icon: "arrow-left-right",
variant: "outline",
href: new_transaction_transfer_match_path(@entry),
data: { turbo_frame: :modal }
open_in: :modal
) %>
</div>

View file

@ -1,45 +1,47 @@
<%# locals: (user:, placement: "right-start", offset: 16) %>
<%= render MenuComponent.new(variant: "avatar", avatar_url: user.profile_image&.variant(:small)&.url, placement: placement, offset: offset, data: { testid: "user-menu" }) do |menu| %>
<%= menu.with_header do %>
<div class="px-4 py-3 flex items-center gap-3">
<div class="w-9 h-9 shrink-0">
<%= render "settings/user_avatar", avatar_url: user.profile_image&.variant(:small)&.url, lazy: true %>
<div data-testid="user-menu">
<%= render MenuComponent.new(variant: "avatar", avatar_url: user.profile_image&.variant(:small)&.url, placement: placement, offset: offset) do |menu| %>
<%= menu.with_header do %>
<div class="px-4 py-3 flex items-center gap-3">
<div class="w-9 h-9 shrink-0">
<%= render "settings/user_avatar", avatar_url: user.profile_image&.variant(:small)&.url, lazy: true %>
</div>
<div class="overflow-hidden text-ellipsis text-sm">
<p class="font-medium"><%= user.display_name %></p>
<% if user.display_name != user.email %>
<p class="text-secondary"><%= user.email %></p>
<% end %>
</div>
</div>
<div class="overflow-hidden text-ellipsis text-sm">
<p class="font-medium"><%= user.display_name %></p>
<% if user.display_name != user.email %>
<p class="text-secondary"><%= user.email %></p>
<% end %>
</div>
</div>
<% if self_hosted? %>
<div class="px-4 py-3 border-t border-tertiary">
<p class="text-sm">
<span class="font-medium text-primary">Version:</span>
<%= link_to Maybe.version.to_release_tag, "https://github.com/maybe-finance/maybe/releases/tag/#{Maybe.version.to_release_tag}", target: "_blank", class: "hover:underline" %>
<% if Maybe.commit_sha.present? %>
(<%= link_to Maybe.commit_sha.first(7), "https://github.com/maybe-finance/maybe/commit/#{Maybe.commit_sha}", target: "_blank", class: "hover:underline" %>)
<% end %>
</p>
</div>
<% end %>
<% end %>
<% menu.with_item(variant: "link", text: "Settings", icon: "settings", href: settings_profile_path(return_to: request.fullpath)) %>
<% menu.with_item(variant: "link", text: "Changelog", icon: "box", href: changelog_path) %>
<% menu.with_item(variant: "link", text: "Feedback", icon: "megaphone", href: feedback_path) %>
<% if self_hosted? %>
<div class="px-4 py-3 border-t border-tertiary">
<p class="text-sm">
<span class="font-medium text-primary">Version:</span>
<%= link_to Maybe.version.to_release_tag, "https://github.com/maybe-finance/maybe/releases/tag/#{Maybe.version.to_release_tag}", target: "_blank", class: "hover:underline" %>
<% if Maybe.commit_sha.present? %>
(<%= link_to Maybe.commit_sha.first(7), "https://github.com/maybe-finance/maybe/commit/#{Maybe.commit_sha}", target: "_blank", class: "hover:underline" %>)
<% end %>
</p>
</div>
<% menu.with_item(variant: "link", text: "Contact", icon: "message-square-more", href: "https://link.maybe.co/discord") %>
<% else %>
<% menu.with_item(variant: "link", text: "Contact", icon: "message-square-more", href: "mailto:hello@maybefinance.com") %>
<% end %>
<% menu.with_item(variant: "divider") %>
<% menu.with_item(variant: "button", text: "Log out", icon: "log-out", href: session_path(Current.session), method: :delete) %>
<% end %>
<% menu.with_item(text: "Settings", icon: "settings", href: settings_profile_path(return_to: request.fullpath)) %>
<% menu.with_item(text: "Changelog", icon: "box", href: changelog_path) %>
<% menu.with_item(text: "Feedback", icon: "megaphone", href: feedback_path) %>
<% if self_hosted? %>
<% menu.with_item(text: "Contact", icon: "message-square-more", href: "https://link.maybe.co/discord") %>
<% else %>
<% menu.with_item(text: "Contact", icon: "message-square-more", href: "mailto:hello@maybefinance.com") %>
<% end %>
<% menu.with_item(variant: "divider") %>
<% menu.with_item(text: "Log out", icon: "log-out", href: session_path(Current.session), method: :delete) %>
<% end %>
</div>

View file

@ -33,10 +33,10 @@
</div>
<div class="flex justify-center py-8">
<%= render ButtonComponent.new(
<%= render LinkComponent.new(
text: "Edit account details",
variant: "ghost",
href: edit_vehicle_path(account),
data: { turbo_frame: :modal }
open_in: :modal
) %>
</div>

View file

@ -2,18 +2,17 @@ class ButtonComponentPreview < ViewComponent::Preview
# @param variant select {{ ButtonComponent::VARIANTS.keys }}
# @param size select {{ ButtonComponent::SIZES.keys }}
# @param disabled toggle
# @param leading_icon text
# @param trailing_icon text
# @param icon text "This is only used for icon-only buttons"
def default(variant: "primary", size: "md", disabled: false, leading_icon: "plus", trailing_icon: nil, icon: "circle")
# @param icon select ["plus", "circle"]
# @param rounded toggle
def default(variant: "primary", size: "md", disabled: false, icon: "plus", rounded: false)
render ButtonComponent.new(
text: "Sample button",
variant: variant,
size: size,
disabled: disabled,
leading_icon: leading_icon,
trailing_icon: trailing_icon,
icon: icon
icon: icon,
rounded: rounded,
data: { menu_target: "button" }
)
end
end

View file

@ -1,11 +0,0 @@
class IconComponentPreview < ViewComponent::Preview
# @param variant select {{ IconComponent::VARIANTS.keys }}
# @param size select {{ IconComponent::SIZES.keys }}
def default(variant: "default", size: "md")
render IconComponent.new(
"circle-user",
variant: variant,
size: size
)
end
end

View file

@ -0,0 +1,27 @@
class LinkComponentPreview < ViewComponent::Preview
# Usage
# -------------
#
# LinkComponent is a small abstraction on top of the `link_to` helper.
#
# It can be used as a regular link or styled as a "Link button" using any of the available ButtonComponent variants.
#
# @param variant select {{ LinkComponent::VARIANTS.keys }}
# @param size select {{ LinkComponent::SIZES.keys }}
# @param icon select ["", "plus", "arrow-right"]
# @param icon_position select ["left", "right"]
# @param full_width toggle
# @param rounded toggle
def default(variant: "default", size: "md", icon: "plus", icon_position: "left", full_width: false, rounded: false)
render LinkComponent.new(
href: "#",
text: "Preview link",
variant: variant,
size: size,
icon: icon,
icon_position: icon_position,
full_width: full_width,
rounded: rounded
)
end
end

View file

@ -1,19 +1,44 @@
class MenuComponentPreview < ViewComponent::Preview
# @param variant select {{ MenuComponent::VARIANTS.keys }}
def default(variant: "icon")
if variant == "icon"
render MenuComponent.new(variant: variant) do |menu|
menu.with_item(text: "Menu item 1", href: "#", icon: "plus")
menu.with_item(text: "Menu item 2", href: "#", icon: "circle")
menu.with_item(text: "Destructive", href: "#", method: :delete, icon: "circle")
end
else
render MenuComponent.new(variant: variant) do |menu|
menu.with_button(text: "New", icon: "plus")
menu.with_item(text: "Menu item 1", href: "#", icon: "plus")
menu.with_item(text: "Menu item 2", href: "#", icon: "circle")
menu.with_item(text: "Destructive", href: "#", method: :delete, icon: "circle")
end
def icon
render MenuComponent.new(variant: "icon") do |menu|
menu_contents(menu)
end
end
def button
render MenuComponent.new(variant: "button") do |menu|
menu.with_button(text: "Open menu", variant: "secondary")
menu_contents(menu)
end
end
def avatar
render MenuComponent.new(variant: "avatar") do |menu|
menu_contents(menu)
end
end
private
def menu_contents(menu)
menu.with_header do
content_tag(:div, class: "p-3") do
content_tag(:h3, "Menu header", class: "font-medium text-gray-900")
end
end
menu.with_item(variant: "link", text: "Link", href: "#", icon: "plus")
menu.with_item(variant: "button", text: "Action", href: "#", method: :post, icon: "circle")
menu.with_item(variant: "button", text: "Action destructive", href: "#", method: :delete, icon: "circle")
menu.with_item(variant: "divider")
menu.with_custom_content do
content_tag(:div, class: "p-4") do
safe_join([
content_tag(:h3, "Custom content header", class: "font-medium text-gray-900"),
content_tag(:p, "Some custom content", class: "text-sm text-gray-500")
])
end
end
end
end