1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-05 13:35:21 +02:00

Pre-launch design sync with Figma spec (#2154)

* Add lookbook + viewcomponent, organize design system file

* Build menu component

* Button updates

* More button fixes

* Replace all menus with new ViewComponent

* Checkpoint: fix tests, all buttons and menus converted

* Split into Link and Button components for clarity

* Button cleanup

* Simplify custom confirmation configuration in views

* Finalize button, link component API

* Add toggle field to custom form builder + Component

* Basic tabs component

* Custom tabs, convert all menu / tab instances in app

* Gem updates

* Centralized icon helper

* Update all icon usage to central helper

* Lint fixes

* Centralize all disclosure instances

* Dialog replacements

* Consolidation of all dialog styles

* Test fixes

* Fix app layout issues, move to component with slots

* Layout simplification

* Flakey test fix

* Fix dashboard mobile issues

* Finalize homepage

* Lint fixes

* Fix shadows and borders in dark mode

* Fix tests

* Remove stale class

* Fix filled icon logic

* Move transparent? to public interface
This commit is contained in:
Zach Gollwitzer 2025-04-30 18:14:22 -04:00 committed by GitHub
parent 1aafed5f8b
commit 90a9546f32
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
291 changed files with 4143 additions and 3104 deletions

View file

@ -25,7 +25,7 @@
<% unless account.scheduled_for_deletion? %>
<%= link_to edit_account_path(account, return_to: return_to), data: { turbo_frame: :modal }, class: "group-hover/account:flex hidden hover:opacity-80 items-center justify-center" do %>
<%= lucide_icon "pencil-line", class: "w-4 h-4 text-secondary" %>
<%= icon("pencil-line", size: "sm") %>
<% end %>
<% end %>
</div>
@ -35,7 +35,9 @@
</p>
<% unless account.scheduled_for_deletion? %>
<%= render "shared/toggle_form", model: account, attribute: :is_active, turbo_frame: "_top" %>
<%= styled_form_with model: account, data: { turbo_frame: "_top", controller: "auto-submit-form" } do |f| %>
<%= f.toggle :is_active, { data: { auto_submit_form_target: "auto" } } %>
<% end %>
<% end %>
</div>
</div>

View file

@ -1,88 +1,91 @@
<%# locals: (family:) %>
<%# locals: (family:, active_account_group_tab:) %>
<% if family.requires_data_provider? && Provider::Registry.get_provider(:synth).nil? %>
<details class="group bg-yellow-tint-10 rounded-lg p-2 text-yellow-600 mb-3 text-xs">
<summary class="flex items-center justify-between gap-2">
<div class="flex items-center gap-2">
<%= icon "triangle-alert", size: "sm" %>
<p class="font-medium">Missing historical data</p>
<div>
<% if family.missing_data_provider? %>
<details class="group bg-yellow-tint-10 rounded-lg p-2 text-yellow-600 mb-3 text-xs">
<summary class="flex items-center justify-between gap-2">
<div class="flex items-center gap-2">
<%= icon "triangle-alert", size: "sm", color: "warning" %>
<p class="font-medium">Missing historical data</p>
</div>
<%= icon("chevron-down", color: "warning", class: "group-open:transform group-open:rotate-180") %>
</summary>
<div class="text-xs py-2 space-y-2">
<p>Maybe uses Synth API to fetch historical exchange rates, security prices, and more. This data is required to calculate accurate historical account balances.</p>
<p>
<%= link_to "Add your Synth API key here.", settings_hosting_path, class: "text-yellow-600 underline" %>
</p>
</div>
</details>
<% end %>
<%= lucide_icon "chevron-down", class: "text-yellow-600 group-open:transform group-open:rotate-180 w-5" %>
</summary>
<div class="text-xs py-2 space-y-2">
<p>Maybe uses Synth API to fetch historical exchange rates, security prices, and more. This data is required to calculate accurate historical account balances.</p>
<p>
<%= link_to "Add your Synth API key here.", settings_hosting_path, class: "text-yellow-600 underline" %>
</p>
</div>
</details>
<% end %>
<div
class="space-y-3"
data-controller="tabs"
data-tabs-local-storage-key-value="account-sidebar-tabs"
data-tabs-active-class="bg-surface shadow-sm text-primary"
data-tabs-inactive-class="text-secondary"
data-tabs-default-tab-value="assets-tab">
<div class="bg-surface-inset rounded-lg p-1 flex">
<button type="button" data-id="assets-tab" class="w-1/3 px-2 py-1 rounded-md text-sm text-secondary font-medium" data-tabs-target="btn" data-action="click->tabs#select">
Assets
</button>
<button type="button" data-id="debts-tab" class="w-1/3 px-2 py-1 rounded-md text-secondary text-sm font-medium" data-tabs-target="btn" data-action="click->tabs#select">
Debts
</button>
<button type="button" data-id="all-tab" class="w-1/3 px-2 py-1 rounded-md text-secondary text-sm font-medium" data-tabs-target="btn" data-action="click->tabs#select">
All
</button>
</div>
<div data-tabs-target="tab" id="assets-tab">
<%= link_to new_account_path(step: "method_select", classification: "asset"),
class: "flex items-center gap-3 btn btn--ghost text-secondary mb-1",
data: { turbo_frame: "modal" } do %>
<%= icon("plus") %>
<span>New asset</span>
<%= 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 %>
<div class="space-y-2">
<% family.balance_sheet.account_groups("asset").each do |group| %>
<%= render "accounts/accountable_group", account_group: group %>
<% end %>
</div>
</div>
<% tabs.with_panel(tab_id: "assets") do %>
<div class="space-y-2">
<%= 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"
) %>
<div data-tabs-target="tab" id="debts-tab" class="hidden">
<%= link_to new_account_path(step: "method_select", classification: "liability"),
class: "flex items-center gap-3 btn btn--ghost text-secondary mb-1",
data: { turbo_frame: "modal" } do %>
<%= icon("plus") %>
<span>New debt</span>
<div>
<% family.balance_sheet.account_groups("asset").each do |group| %>
<%= render "accounts/accountable_group", account_group: group %>
<% end %>
</div>
</div>
<% end %>
<div class="space-y-2">
<% family.balance_sheet.account_groups("liability").each do |group| %>
<%= render "accounts/accountable_group", account_group: group %>
<% end %>
</div>
</div>
<% tabs.with_panel(tab_id: "debts") do %>
<div class="space-y-2">
<%= 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"
) %>
<div data-tabs-target="tab" id="all-tab" class="hidden">
<%= link_to new_account_path(step: "method_select"),
class: "flex items-center gap-3 btn btn--ghost text-secondary mb-1",
data: { turbo_frame: "modal" } do %>
<%= icon("plus") %>
<span>New account</span>
<div>
<% family.balance_sheet.account_groups("liability").each do |group| %>
<%= render "accounts/accountable_group", account_group: group %>
<% end %>
</div>
</div>
<% end %>
<div class="space-y-2">
<% family.balance_sheet.account_groups.each do |group| %>
<%= render "accounts/accountable_group", account_group: group %>
<% end %>
</div>
</div>
<% tabs.with_panel(tab_id: "all") do %>
<div class="space-y-2">
<%= 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"
) %>
<div>
<% family.balance_sheet.account_groups.each do |group| %>
<%= render "accounts/accountable_group", account_group: group %>
<% end %>
</div>
</div>
<% end %>
<% end %>
</div>

View file

@ -1,9 +1,11 @@
<%# locals: (accountable:) %>
<%= link_to new_polymorphic_path(accountable, step: "method_select", return_to: params[:return_to]),
class: "flex items-center gap-4 w-full text-center focus:outline-hidden hover:bg-surface-hover focus:bg-surface-hover fg-contrast hover:fg-primary focus:fg-primary border border-transparent block px-2 rounded-lg p-2" do %>
<span style="background-color: color-mix(in srgb, <%= accountable.color %> 10%, white);" class="flex w-8 h-8 shrink-0 grow-0 items-center justify-center rounded-lg border border-alpha-black-25">
<%= lucide_icon(accountable.icon, style: "color: #{accountable.color}", class: "w-5 h-5") %>
</span>
class: "flex items-center gap-4 w-full text-center focus:outline-hidden hover:bg-surface-hover focus:bg-surface-hover fg-primary border border-transparent block px-2 rounded-lg p-2" do %>
<%= render FilledIconComponent.new(
icon: accountable.icon,
hex_color: accountable.color,
) %>
<%= accountable.display_name.singularize %>
<% end %>

View file

@ -1,11 +1,7 @@
<%# locals: (account_group:) %>
<details class="group" data-controller="account-collapse" data-account-collapse-type-value="<%= account_group.key %>">
<summary class="px-3 py-2 flex items-center gap-3 cursor-pointer h-10 mb-1">
<%= lucide_icon("chevron-right", class: "group-open:rotate-90 text-secondary w-5 h-5") %>
<%= tag.p account_group.name, class: "text-sm font-medium" %>
<%= render DisclosureComponent.new(title: account_group.name, align: :left) do |disclosure| %>
<% disclosure.with_summary_content do %>
<div class="ml-auto text-right grow">
<%= tag.p format_money(account_group.total_money), class: "text-sm font-medium text-primary" %>
@ -15,11 +11,11 @@
</div>
<% end %>
</div>
</summary>
<% end %>
<div class="space-y-1">
<% account_group.accounts.each do |account| %>
<%= link_to account_path(account), class: "block flex items-center gap-2 btn btn--ghost", title: account.name do %>
<%= link_to account_path(account), class: "block flex items-center gap-2 px-3 py-2 hover:bg-surface-hover", title: account.name do %>
<%= render "accounts/logo", account: account, size: "sm", color: account_group.color %>
<div class="min-w-0 grow">
@ -40,10 +36,13 @@
<% end %>
</div>
<%= link_to new_polymorphic_path(account_group.key, step: "method_select"),
class: "flex items-center gap-3 btn btn--ghost text-secondary",
data: { turbo_frame: "modal" } do %>
<%= icon("plus") %>
<span>New <%= account_group.name.downcase.singularize %></span>
<% end %>
</details>
<%= render LinkComponent.new(
href: new_polymorphic_path(account_group.key, step: "method_select"),
text: "New #{account_group.name.downcase.singularize}",
icon: "plus",
full_width: true,
variant: "ghost",
frame: :modal,
class: "justify-start"
) %>
<% end %>

View file

@ -3,9 +3,10 @@
<%= tag.p t(".no_accounts"), class: "text-primary mb-1 font-medium" %>
<%= tag.p t(".empty_message"), class: "text-secondary mb-4" %>
<%= link_to new_account_path, class: "w-fit flex text-white text-sm font-medium items-center gap-1 bg-gray-900 rounded-lg p-2 pr-3", data: { turbo_frame: "modal" } do %>
<%= lucide_icon("plus", class: "w-5 h-5") %>
<span><%= t(".new_account") %></span>
<% end %>
<%= render LinkComponent.new(
text: t(".new_account"),
href: new_account_path,
frame: :modal
) %>
</div>
</div>

View file

@ -12,5 +12,5 @@
<% elsif account.logo.attached? %>
<%= image_tag account.logo, class: "shrink-0 rounded-full #{size_classes[size]}" %>
<% else %>
<%= circle_logo(account.name, hex: color || account.accountable.color, size: size) %>
<%= render FilledIconComponent.new(variant: :text, hex_color: color || account.accountable.color, text: account.name, size: size, rounded: true) %>
<% end %>

View file

@ -2,23 +2,23 @@
<h1 class="text-xl"><%= t(".accounts") %></h1>
<div class="flex items-center gap-5">
<div class="flex items-center gap-2">
<%= button_to sync_all_accounts_path,
disabled: Current.family.syncing?,
class: "md:btn md:btn--outline flex items-center justify-center gap-2 w-9 h-9 md:w-auto md:h-auto rounded-full md:rounded-lg",
title: t(".sync") do %>
<%= lucide_icon "refresh-cw", class: "w-5 h-5" %>
<span class="hidden md:inline"><%= t(".sync") %></span>
<% end %>
<%= render ButtonComponent.new(
text: "Sync all",
href: sync_all_accounts_path,
method: :post,
variant: "outline",
disabled: Current.family.syncing?,
icon: "refresh-cw",
class: ""
) %>
<%= link_to new_account_path(return_to: accounts_path),
data: { turbo_frame: "modal" },
class: "btn btn--primary flex items-center justify-center gap-1 w-9 h-9 md:w-auto md:h-auto rounded-full md:rounded-lg" do %>
<div class="flex items-center justify-center w-5 h-5">
<%= lucide_icon("plus") %>
</div>
<p class="hidden md:block text-sm font-medium"><%= t(".new_account") %></p>
<% end %>
<%= render LinkComponent.new(
text: "New account",
href: new_account_path(return_to: accounts_path),
variant: "primary",
icon: "plus",
frame: :modal
) %>
</div>
</div>
</header>

View file

@ -2,10 +2,10 @@
<details open class="group bg-container p-4 shadow-border-xs rounded-xl">
<summary class="flex items-center gap-2 focus-visible:outline-hidden">
<%= lucide_icon "chevron-right", class: "group-open:transform group-open:rotate-90 text-secondary w-5" %>
<%= icon("chevron-right", class: "group-open:transform group-open:rotate-90") %>
<div class="flex items-center justify-center h-8 w-8 rounded-full bg-black/5">
<%= lucide_icon("folder-pen", class: "w-5 h-5 text-secondary") %>
<%= icon("folder-pen") %>
</div>
<span class="mr-auto text-sm font-medium text-primary"><%= t(".other_accounts") %></span>

View file

@ -24,10 +24,12 @@
<% unless params[:return_to].present? %>
<%= button_to imports_path(import: { type: "AccountImport" }),
data: { turbo_frame: :_top },
class: "flex items-center gap-4 w-full text-center focus:outline-hidden hover:bg-surface-hover focus:bg-surface-hover fg-contrast hover:fg-primary focus:fg-primary border border-transparent block px-2 rounded-lg p-2" do %>
<span style="background-color: color-mix(in srgb, #F79009 10%, white);" class="flex w-8 h-8 shrink-0 grow-0 items-center justify-center rounded-lg border border-alpha-black-25">
<%= lucide_icon("download", style: "color: #F79009", class: "w-5 h-5") %>
</span>
class: "flex items-center gap-4 w-full text-center focus:outline-hidden hover:bg-surface-hover focus:bg-surface-hover fg-primary border border-transparent block px-2 rounded-lg p-2" do %>
<%= render FilledIconComponent.new(
icon: "download",
hex_color: "#F79009",
) %>
<%= t("accounts.new.import_accounts") %>
<% end %>
<% end %>

View file

@ -1,18 +1,22 @@
<%# locals: (title:, back_path: nil) %>
<%= modal do %>
<div class="flex flex-col w-screen max-w-xl relative" data-controller="list-keyboard-navigation">
<div class="border-b border-tertiary md:border-alpha-black-25 p-4 text-gray-800 flex items-center space-x-3">
<% if back_path %>
<%= link_to back_path, class: "flex w-8 h-8 shrink-0 grow-0 items-center justify-center rounded-lg bg-alpha-black-50 focus:outline-gray-300 focus:outline" do %>
<%= lucide_icon("arrow-left", class: "text-secondary w-5 h-5") %>
<%= render DialogComponent.new do |dialog| %>
<div class="flex flex-col relative" data-controller="list-keyboard-navigation">
<div class="border-b border-tertiary md:border-alpha-black-25 px-4 pb-4 text-gray-800 flex items-center justify-between gap-2">
<div class="flex items-center gap-2">
<% if back_path %>
<%= render LinkComponent.new(
variant: "icon",
icon: "arrow-left",
href: back_path,
size: "lg"
) %>
<% end %>
<% end %>
<span class="text-primary"><%= title %></span>
<button class="absolute top-1/2 -translate-y-1/2 right-4 flex w-8 h-8 items-center justify-center rounded-lg md:hidden outline-0" data-action="click->modal#close">
<%= lucide_icon("x", class: "text-secondary w-6 h-6") %>
</button>
<span class="text-primary"><%= title %></span>
</div>
<%= icon("x", as_button: true, size: "lg", data: { action: "dialog#close" }) %>
</div>
<div class="p-2 text-subdued">
@ -22,20 +26,26 @@
<%= yield %>
</div>
<div class="border-t border-alpha-black-25 p-4 text-secondary text-sm justify-between hidden md:flex">
<div class="border-t border-alpha-black-25 px-4 pt-4 text-secondary text-sm justify-between hidden md:flex">
<div class="flex space-x-5">
<div class="flex items-center space-x-2">
<span>Select</span>
<kbd class="bg-alpha-black-50 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.1)] p-1 rounded-md flex w-5 h-5 shrink-0 grow-0 items-center justify-center"><%= lucide_icon("corner-down-left", class: "inline w-3 h-3") %></kbd>
<kbd class="bg-alpha-black-50 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.1)] p-1 rounded-md flex w-5 h-5 shrink-0 grow-0 items-center justify-center">
<%= icon("corner-down-left", size: "xs") %>
</kbd>
</div>
<div class="flex items-center space-x-2">
<span>Navigate</span>
<kbd class="bg-alpha-black-50 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.1)] p-1 rounded-md flex w-5 h-5 shrink-0 grow-0 items-center justify-center"><%= lucide_icon("arrow-up", class: "inline w-3 h-3") %></kbd>
<kbd class="bg-alpha-black-50 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.1)] p-1 rounded-md flex w-5 h-5 shrink-0 grow-0 items-center justify-center"><%= lucide_icon("arrow-down", class: "inline w-3 h-3") %></kbd>
<kbd class="bg-alpha-black-50 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.1)] p-1 rounded-md flex w-5 h-5 shrink-0 grow-0 items-center justify-center">
<%= icon("arrow-up", size: "xs") %>
</kbd>
<kbd class="bg-alpha-black-50 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.1)] p-1 rounded-md flex w-5 h-5 shrink-0 grow-0 items-center justify-center">
<%= icon("arrow-down", size: "xs") %>
</kbd>
</div>
</div>
<div class="flex items-center space-x-2">
<button data-action="modal#close">Close</button>
<button data-action="dialog#close">Close</button>
<kbd class="bg-alpha-black-50 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.1)] p-1 rounded-md flex w-8 h-5 shrink-0 grow-0 items-center justify-center text-xs">ESC</kbd>
</div>
</div>

View file

@ -2,18 +2,18 @@
<%= render layout: "accounts/new/container", locals: { title: t(".title"), back_path: new_account_path } do %>
<div class="text-sm">
<%= link_to path, class: "flex items-center gap-4 w-full text-center focus:outline-hidden focus:bg-surface border border-transparent focus:border focus:border-gray-200 px-2 hover:bg-surface rounded-lg p-2" do %>
<%= link_to path, class: "flex items-center gap-4 w-full text-center text-primary focus:outline-hidden focus:bg-surface border border-transparent focus:border focus:border-gray-200 px-2 hover:bg-surface rounded-lg p-2" do %>
<span class="flex w-8 h-8 shrink-0 grow-0 items-center justify-center rounded-lg bg-alpha-black-50 shadow-[inset_0_0_0_1px_rgba(0,0,0,0.02)]">
<%= lucide_icon("keyboard", class: "text-secondary w-5 h-5") %>
<%= icon("keyboard") %>
</span>
<%= t("accounts.new.method_selector.manual_entry") %>
<% end %>
<% if us_link_token %>
<%# Default US-only Link %>
<button data-controller="plaid" data-action="plaid#open modal#close" data-plaid-region-value="us" data-plaid-link-token-value="<%= us_link_token %>" class="flex items-center gap-4 w-full text-center focus:outline-hidden focus:bg-gray-50 border border-transparent focus:border focus:border-gray-200 px-2 hover:bg-gray-50 rounded-lg p-2">
<button data-controller="plaid" data-action="plaid#open dialog#close" data-plaid-region-value="us" data-plaid-link-token-value="<%= us_link_token %>" class="text-primary flex items-center gap-4 w-full text-center focus:outline-hidden focus:bg-gray-50 border border-transparent focus:border focus:border-gray-200 px-2 hover:bg-gray-50 rounded-lg p-2">
<span class="flex w-8 h-8 shrink-0 grow-0 items-center justify-center rounded-lg bg-alpha-black-50 shadow-[inset_0_0_0_1px_rgba(0,0,0,0.02)]">
<%= lucide_icon("link-2", class: "text-secondary w-5 h-5") %>
<%= icon("link-2") %>
</span>
<%= t("accounts.new.method_selector.connected_entry") %>
</button>
@ -21,9 +21,9 @@
<%# EU Link %>
<% if eu_link_token %>
<button data-controller="plaid" data-action="plaid#open modal#close" data-plaid-region-value="eu" data-plaid-link-token-value="<%= eu_link_token %>" class="flex items-center gap-4 w-full text-center focus:outline-hidden focus:bg-gray-50 border border-transparent focus:border focus:border-gray-200 px-2 hover:bg-gray-50 rounded-lg p-2">
<button data-controller="plaid" data-action="plaid#open dialog#close" data-plaid-region-value="eu" data-plaid-link-token-value="<%= eu_link_token %>" class="text-primary flex items-center gap-4 w-full text-center focus:outline-hidden focus:bg-gray-50 border border-transparent focus:border focus:border-gray-200 px-2 hover:bg-gray-50 rounded-lg p-2">
<span class="flex w-8 h-8 shrink-0 grow-0 items-center justify-center rounded-lg bg-alpha-black-50 shadow-[inset_0_0_0_1px_rgba(0,0,0,0.02)]">
<%= lucide_icon("link-2", class: "text-secondary w-5 h-5") %>
<%= icon("link-2") %>
</span>
<%= t("accounts.new.method_selector.connected_entry_eu") %>
</button>

View file

@ -2,30 +2,29 @@
<%= 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? %>
<div data-controller="menu" data-testid="activity-menu">
<button class="btn btn--secondary flex items-center gap-2" data-menu-target="button">
<%= lucide_icon("plus", class: "w-4 h-4") %>
<%= tag.span t(".new") %>
</button>
<div data-menu-target="content" class="z-10 hidden bg-container rounded-lg border border-alpha-black-25 shadow-xs p-1">
<%= link_to new_valuation_path(account_id: @account.id), data: { turbo_frame: :modal }, class: "block p-2 rounded-lg hover:bg-gray-50 flex items-center gap-2" do %>
<%= lucide_icon("circle-dollar-sign", class: "text-secondary w-5 h-5") %>
<%= tag.span t(".new_balance"), class: "text-sm" %>
<% end %>
<%= render MenuComponent.new(variant: "button") do |menu| %>
<% menu.with_button(text: "New", variant: "secondary", icon: "plus") %>
<% unless @account.crypto? %>
<%= link_to @account.investment? ? new_trade_path(account_id: @account.id) : new_transaction_path(account_id: @account.id), data: { turbo_frame: :modal }, class: "btn btn--primary flex items-center justify-center gap-2 rounded-full md:rounded-lg w-9 h-9 md:w-auto md:h-auto" do %>
<span class="flex items-center justify-center">
<%= lucide_icon("credit-card", class: "text-secondary w-5 h-5") %>
</span>
<%= tag.span t(".new_transaction"), class: "text-sm md:block" %>
<% end %>
<% end %>
</div>
</div>
<% menu.with_item(
variant: "link",
text: "New balance",
icon: "circle-dollar-sign",
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,
data: { turbo_frame: :modal }) %>
<% end %>
<% end %>
<% end %>
</div>
@ -38,7 +37,7 @@
<div class="flex gap-2 mb-4">
<div class="grow">
<div class="flex items-center px-3 py-2 gap-2 border border-secondary rounded-lg focus-within:ring-gray-100 focus-within:border-gray-900">
<%= lucide_icon("search", class: "w-5 h-5 text-secondary") %>
<%= icon("search") %>
<%= hidden_field_tag :account_id, @account.id %>
<%= form.search_field :search,
placeholder: "Search entries by name",

View file

@ -24,7 +24,11 @@
data: { "auto-submit-form-target": "auto" } %>
<% end %>
<%= period_select form: form, selected: period %>
<%= form.select :period,
Period.as_options,
{ selected: period.key },
data: { "auto-submit-form-target": "auto" },
class: "bg-container border border-secondary font-medium rounded-lg px-3 py-2 text-sm pr-7 cursor-pointer text-primary focus:outline-hidden focus:ring-0" %>
</div>
<% end %>
</div>

View file

@ -19,17 +19,27 @@
</div>
<% end %>
<div class="flex items-center gap-3 ml-auto">
<div class="flex items-center gap-1 ml-auto">
<% if account.plaid_account_id.present? %>
<% if Rails.env.development? %>
<%= button_to sync_plaid_item_path(account.plaid_account.plaid_item), disabled: account.syncing?, data: { turbo: false }, class: "flex items-center gap-2", title: "Sync Account" do %>
<%= lucide_icon "refresh-cw", class: "w-4 h-4 text-secondary hover:text-subdued" %>
<% end %>
<%= icon(
"refresh-cw",
as_button: true,
size: "sm",
href: sync_plaid_item_path(account.plaid_account.plaid_item),
disabled: account.syncing?,
frame: :_top
) %>
<% end %>
<% else %>
<%= button_to sync_account_path(account), disabled: account.syncing?, data: { turbo: false }, class: "flex items-center gap-2", title: "Sync Account" do %>
<%= lucide_icon "refresh-cw", class: "w-4 h-4 text-secondary hover:text-subdued" %>
<% end %>
<%= icon(
"refresh-cw",
as_button: true,
size: "sm",
href: sync_account_path(account),
disabled: account.syncing?,
frame: :_top
) %>
<% end %>
<%= render "accounts/show/menu", account: account %>

View file

@ -1,47 +1,25 @@
<%# locals: (account:) %>
<%= contextual_menu do %>
<div class="w-48 p-1 text-sm leading-6 text-primary bg-container shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
<% if account.plaid_account_id.present? %>
<%= link_to accounts_path,
data: { turbo_frame: :_top },
class: "block w-full py-2 px-3 space-x-2 text-primary hover:bg-gray-50 flex items-center rounded-lg" do %>
<%= lucide_icon "pencil-line", class: "w-5 h-5 text-secondary" %>
<%= 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 }) %>
<span><%= t(".manage") %></span>
<% end %>
<% else %>
<%= link_to edit_account_path(account),
data: { turbo_frame: :modal },
class: "block w-full py-2 px-3 space-x-2 text-primary hover:bg-gray-50 flex items-center rounded-lg" do %>
<%= lucide_icon "pencil-line", class: "w-5 h-5 text-secondary" %>
<% 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",
data: { turbo_frame: :_top }
) %>
<% end %>
<span><%= t(".edit") %></span>
<% end %>
<% unless account.crypto? %>
<%= button_to imports_path({ import: { type: account.investment? ? "TradeImport" : "TransactionImport", account_id: account.id } }),
data: { turbo_frame: :_top },
class: "block w-full py-2 px-3 space-x-2 text-primary hover:bg-gray-50 flex items-center rounded-lg" do %>
<%= lucide_icon "download", class: "w-5 h-5 text-secondary" %>
<span><%= t(".import") %></span>
<% end %>
<% end %>
<%= button_to account_path(account),
method: :delete,
class: "block w-full py-2 px-3 space-x-2 text-red-600 hover:bg-red-50 flex items-center rounded-lg",
data: {
turbo_frame: :_top,
turbo_confirm: {
title: t(".confirm_title"),
body: t(".confirm_body_html"),
accept: t(".confirm_accept", name: account.name)
}
} do %>
<%= lucide_icon("trash-2", class: "w-5 h-5 mr-2") %> Delete account
<% end %>
<% end %>
</div>
<% menu.with_item(
variant: "button",
text: "Delete account",
href: account_path(account),
method: :delete,
icon: "trash-2",
confirm: CustomConfirm.for_resource_deletion("Account", high_severity: true),
data: { turbo_frame: :_top }
) %>
<% end %>

View file

@ -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 %>
<div class="flex gap-2 text-sm text-primary font-medium mb-4">
<% 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 %>
</div>
<%= selected_tab[:contents] %>
<% tabs.each do |tab| %>
<% tabs_container.with_panel(tab_id: tab[:key]) do %>
<%= tab[:contents] %>
<% end %>
<% end %>
<% end %>

View file

@ -16,7 +16,7 @@
<%= render "accounts/show/chart", account: account %>
<% end %>
<div class="min-h-[800px]">
<div class="min-h-[800px]" data-testid="account-details>
<% if tabs.present? %>
<%= tabs %>
<% else %>

View file

@ -5,7 +5,7 @@
<details class="group mb-1">
<summary class="flex items-center gap-2">
<p class="text-secondary text-sm">Assistant reasoning</p>
<%= lucide_icon "chevron-down", class: "group-open:transform group-open:rotate-180 text-secondary w-5" %>
<%= icon("chevron-down", class: "group-open:transform group-open:rotate-180") %>
</summary>
<div class="prose prose--ai-chat"><%= markdown(assistant_message.content) %></div>

View file

@ -2,7 +2,7 @@
<details class="my-2 group mb-4">
<summary class="text-secondary text-xs cursor-pointer flex items-center gap-2">
<%= lucide_icon "chevron-right", class: "group-open:transform group-open:rotate-90 text-secondary w-5" %>
<%= icon("chevron-right", class: "group-open:transform group-open:rotate-90") %>
<p>Tool Calls</p>
</summary>

View file

@ -8,11 +8,17 @@
<%= render "budget_categories/budget_category_donut", budget_category: budget_category %>
</div>
<% else %>
<div class="w-8 h-8 group-hover:scale-105 transition-all duration-300 rounded-full flex justify-center items-center" style="<%= mixed_hex_styles(budget_category.category.color) %>">
<div class="w-8 h-8 group-hover:scale-105 transition-all duration-300 rounded-full flex justify-center items-center" style="color: <%= budget_category.category.color %>">
<% if budget_category.category.lucide_icon %>
<%= icon(budget_category.category.lucide_icon) %>
<%= icon(budget_category.category.lucide_icon, color: "current") %>
<% else %>
<%= render "shared/circle_logo", name: budget_category.category.name, hex: budget_category.category.color %>
<%= render FilledIconComponent.new(
variant: :text,
hex_color: budget_category.category.color,
text: budget_category.category.name,
size: "sm",
rounded: true
) %>
<% end %>
</div>
<% end %>

View file

@ -11,7 +11,9 @@
<div data-donut-chart-target="contentContainer" class="flex justify-center items-center h-full p-1">
<div data-donut-chart-target="defaultContent" class="h-full w-full rounded-full flex flex-col items-center justify-center" style="background-color: <%= hex_with_alpha(budget_category.category.color, 0.05) %>">
<% if budget_category.category.lucide_icon %>
<%= lucide_icon budget_category.category.lucide_icon, class: "w-4 h-4 shrink-0", style: "color: #{budget_category.category.color}" %>
<span style="color: <%= budget_category.category.color %>">
<%= icon(budget_category.category.lucide_icon, size: "sm", color: "current") %>
</span>
<% else %>
<span class="text-sm uppercase" style="color: <%= budget_category.category.color %>">
<%= budget_category.category.name.first.upcase %>

View file

@ -1,11 +1,10 @@
<div id="<%= dom_id(budget, :confirm_button) %>">
<% if budget.allocations_valid? %>
<%= link_to "Confirm",
budget_path(budget),
class: "block btn btn--primary w-full text-center" %>
<% else %>
<span class="block btn btn--secondary w-full text-center text-subdued cursor-not-allowed">
Confirm
</span>
<% end %>
<%= render ButtonComponent.new(
text: "Confirm",
variant: "primary",
full_width: true,
href: budget_path(budget),
method: :get,
disabled: !budget.allocations_valid?
) %>
</div>

View file

@ -6,12 +6,18 @@
</p>
<div class="flex items-center gap-2">
<%= button_to "Use defaults (recommended)", bootstrap_categories_path, class: "btn btn--primary" %>
<%= render ButtonComponent.new(
text: "Use defaults (recommended)",
href: bootstrap_categories_path,
) %>
<%= link_to new_category_path, class: "btn btn--outline flex items-center gap-1", data: { turbo_frame: "modal" } do %>
<%= lucide_icon("plus", class: "w-5 h-5") %>
<span>New category</span>
<% end %>
<%= render LinkComponent.new(
text: "New category",
variant: "outline",
icon: "plus",
href: new_category_path,
frame: :modal,
) %>
</div>
</div>
</div>

View file

@ -32,7 +32,7 @@
<% group.budget_subcategories.each do |budget_subcategory| %>
<div class="w-full flex items-center gap-4">
<div class="ml-4 flex items-center justify-center text-subdued">
<%= lucide_icon "corner-down-right", class: "w-5 h-5 shrink-0" %>
<%= icon("corner-down-right") %>
</div>
<%= render "budget_categories/budget_category_form", budget_category: budget_subcategory %>

View file

@ -1,39 +1,32 @@
<%= drawer do %>
<div class="space-y-4">
<header class="flex justify-between">
<div>
<p class="text-sm text-secondary">Category</p>
<h3 class="text-2xl font-medium text-primary">
<%= @budget_category.name %>
</h3>
<% if @budget_category.budget.initialized? %>
<p class="text-sm text-secondary">
<span class="text-primary">
<%= format_money(@budget_category.actual_spending_money) %>
</span>
<span>/</span>
<span><%= format_money(@budget_category.budgeted_spending_money) %></span>
</p>
<% end %>
</div>
<%= render DialogComponent.new(variant: :drawer) do |dialog| %>
<% dialog.with_header do %>
<div>
<p class="text-sm text-secondary">Category</p>
<h3 class="text-2xl font-medium text-primary">
<%= @budget_category.name %>
</h3>
<% if @budget_category.budget.initialized? %>
<div class="ml-auto w-10 h-10">
<%= render "budget_categories/budget_category_donut",
budget_category: @budget_category %>
</div>
<p class="text-sm text-secondary">
<span class="text-primary">
<%= format_money(@budget_category.actual_spending_money) %>
</span>
<span>/</span>
<span><%= format_money(@budget_category.budgeted_spending_money) %></span>
</p>
<% end %>
</header>
</div>
<details class="group space-y-2" open>
<summary class="flex list-none items-center justify-between rounded-xl px-3 py-2
text-xs font-medium uppercase text-secondary bg-gray-25 focus-visible:outline-hidden">
<h4>Overview</h4>
<%= lucide_icon "chevron-down",
class: "group-open:transform group-open:rotate-180 text-secondary w-5" %>
</summary>
<% if @budget_category.budget.initialized? %>
<div class="ml-auto w-10 h-10">
<%= render "budget_categories/budget_category_donut",
budget_category: @budget_category %>
</div>
<% end %>
<% end %>
<% dialog.with_body do %>
<% dialog.with_section(title: "Overview", open: true) do %>
<div class="pb-4">
<dl class="space-y-3 px-3 py-2">
<div class="flex items-center justify-between text-sm">
@ -49,20 +42,20 @@
<div class="flex items-center justify-between text-sm">
<dt class="text-secondary">Status</dt>
<% if @budget_category.available_to_spend.negative? %>
<dd class="text-red-500 flex items-center gap-1 text-red-500 font-medium">
<%= lucide_icon "alert-circle", class: "shrink-0 w-4 h-4 text-red-500" %>
<dd class="flex items-center gap-1 text-red-500 font-medium">
<%= icon "alert-circle", size: "sm", color: "destructive" %>
<%= format_money @budget_category.available_to_spend_money.abs %>
<span>overspent</span>
</dd>
<% elsif @budget_category.available_to_spend.zero? %>
<dd class="text-orange-500 flex items-center gap-1 text-orange-500 font-medium">
<%= lucide_icon "x-circle", class: "shrink-0 w-4 h-4 text-orange-500" %>
<dd class="flex items-center gap-1 text-orange-500 font-medium">
<%= icon "x-circle", size: "sm", color: "warning" %>
<%= format_money @budget_category.available_to_spend_money %>
<span>left</span>
</dd>
<% else %>
<dd class="text-primary flex items-center gap-1 text-green-500 font-medium">
<%= lucide_icon "check-circle-2", class: "shrink-0 w-4 h-4 text-green-500" %>
<%= icon "check-circle", size: "sm", color: "success" %>
<%= format_money @budget_category.available_to_spend_money %>
<span>left</span>
</dd>
@ -92,16 +85,9 @@
</div>
</dl>
</div>
</details>
<details class="group space-y-2" open>
<summary class="flex list-none items-center justify-between rounded-xl px-3 py-2
text-xs font-medium uppercase text-secondary bg-gray-25 focus-visible:outline-hidden">
<h4>Recent Transactions</h4>
<%= lucide_icon "chevron-down",
class: "group-open:transform group-open:rotate-180 text-secondary w-5" %>
</summary>
<% end %>
<% dialog.with_section(title: "Recent Transactions", open: true) do %>
<div class="space-y-2">
<div class="px-3 py-4 space-y-2">
<% if @recent_transactions.any? %>
@ -133,14 +119,17 @@
<% end %>
</ul>
<%= link_to "View all category transactions",
transactions_path(q: {
categories: [@budget_category.name],
start_date: @budget.start_date,
end_date: @budget.end_date
}),
data: { turbo_frame: :_top },
class: "block text-center btn btn--outline w-full" %>
<%= render LinkComponent.new(
text: "View all category transactions",
variant: "outline",
full_width: true,
href: transactions_path(q: {
categories: [@budget_category.name],
start_date: @budget.start_date,
end_date: @budget.end_date
}),
frame: :_top
) %>
<% else %>
<p class="text-secondary text-sm mb-4">
No transactions found for this budget period.
@ -148,6 +137,6 @@
<% end %>
</div>
</div>
</details>
</div>
<% end %>
<% end %>
<% end %>

View file

@ -25,7 +25,7 @@
<% group.budget_subcategories.each do |budget_subcategory| %>
<div class="w-full flex items-center -mt-4">
<div class="ml-8 flex items-center justify-center text-subdued">
<%= lucide_icon "corner-down-right", class: "w-5 h-5 shrink-0" %>
<%= icon "corner-down-right" %>
</div>
<%= render "budget_categories/budget_category", budget_category: budget_subcategory %>

View file

@ -8,24 +8,29 @@
<span>Spent</span>
</div>
<div class="text-3xl font-medium <%= budget.available_to_spend.negative? ? "text-red-500" : "text-primary" %>">
<div class="mb-2 text-3xl font-medium <%= budget.available_to_spend.negative? ? "text-red-500" : "text-primary" %>">
<%= format_money(budget.actual_spending_money) %>
</div>
<%= link_to edit_budget_path(budget), class: "btn btn--secondary flex items-center gap-1 mt-2" do %>
<span class="text-primary font-medium">
of <%= format_money(budget.budgeted_spending_money) %>
</span>
<%= lucide_icon "pencil", class: "w-4 h-4 text-secondary hover:text-gray-600" %>
<% end %>
<%= render LinkComponent.new(
text: "of #{budget.budgeted_spending_money.format}",
variant: "secondary",
icon: "pencil",
icon_position: "right",
size: "sm",
href: edit_budget_path(budget)
) %>
<% else %>
<div class="text-subdued text-3xl mb-2">
<span><%= format_money Money.new(0, budget.currency || budget.family.currency) %></span>
</div>
<%= link_to edit_budget_path(budget), class: "flex items-center gap-2 btn btn--primary" do %>
<%= lucide_icon "plus", class: "w-4 h-4 text-white" %>
New budget
<% end %>
<%= render LinkComponent.new(
text: "New budget",
size: "sm",
icon: "plus",
href: edit_budget_path(budget)
) %>
<% end %>
</div>
@ -41,11 +46,14 @@
<%= format_money(bc.actual_spending_money) %>
</p>
<%= link_to budget_budget_categories_path(budget), class: "btn btn--secondary flex items-center gap-1" do %>
<span>of <%= format_money(bc.budgeted_spending_money, precision: 0) %></span>
<%= lucide_icon "pencil", class: "w-4 h-4 text-secondary shrink-0" %>
<% end %>
<%= render LinkComponent.new(
text: "of #{bc.budgeted_spending_money.format(precision: 0)}",
variant: "secondary",
icon: "pencil",
icon_position: "right",
size: "sm",
href: budget_budget_categories_path(budget)
) %>
</div>
</div>
<% end %>

View file

@ -3,38 +3,46 @@
<div class="flex items-center gap-1 mb-4">
<div class="flex items-center gap-2">
<% if budget.previous_budget_param %>
<%= link_to budget_path(budget.previous_budget_param) do %>
<%= lucide_icon "chevron-left" %>
<% end %>
<%= render LinkComponent.new(
variant: "icon",
icon: "chevron-left",
href: budget_path(budget.previous_budget_param),
) %>
<% else %>
<%= lucide_icon "chevron-left", class: "text-subdued" %>
<span class="text-subdued">
<%= icon "chevron-left", color: "current" %>
</span>
<% end %>
<% if budget.next_budget_param %>
<%= link_to budget_path(budget.next_budget_param) do %>
<%= lucide_icon "chevron-right" %>
<% end %>
<%= render LinkComponent.new(
variant: "icon",
icon: "chevron-right",
href: budget_path(budget.next_budget_param),
) %>
<% else %>
<%= lucide_icon "chevron-right", class: "text-subdued" %>
<span class="text-subdued">
<%= icon "chevron-right", color: "current" %>
</span>
<% end %>
</div>
<div data-controller="menu" data-menu-placement-value="bottom-start">
<%= tag.button data: { menu_target: "button" }, class: "flex items-center gap-1 hover:bg-alpha-black-25 cursor-pointer rounded-md p-2" do %>
<span class="text-primary font-medium text-3xl md:text-base"><%= @budget.name %></span>
<%= lucide_icon "chevron-down", class: "w-5 h-5 shrink-0 text-secondary" %>
<%= render MenuComponent.new(variant: "button") do |menu| %>
<% menu.with_button class: "flex items-center gap-1 hover:bg-alpha-black-25 cursor-pointer rounded-md p-2" do %>
<span class="text-primary font-medium text-lg lg:text-base"><%= @budget.name %></span>
<%= icon("chevron-down") %>
<% end %>
<div data-menu-target="content" class="hidden z-10">
<% menu.with_custom_content do %>
<%= render "budgets/picker", family: Current.family, year: budget.start_date.year %>
</div>
</div>
<% end %>
<% end %>
<div class="ml-auto">
<% if @budget.current? %>
<span class="border border-secondary text-primary text-sm font-medium px-3 py-2 rounded-lg">Today</span>
<% else %>
<%= link_to "Today", budget_path(Budget.date_to_param(Date.current)), class: "btn btn--outline" %>
<% end %>
<%= render LinkComponent.new(
text: "Today",
variant: "outline",
href: budget_path(Budget.date_to_param(Date.current)),
) %>
</div>
</div>

View file

@ -24,7 +24,7 @@
<%= link_to step[:path], class: "flex items-center gap-3" do %>
<div class="flex items-center gap-2 text-sm font-medium <%= text_class %>">
<span class="<%= step_class %> w-7 h-7 rounded-full shrink-0 inline-flex items-center justify-center border border-transparent">
<%= step[:is_complete] && !is_current ? lucide_icon("check", class: "w-4 h-4") : idx + 1 %>
<%= step[:is_complete] && !is_current ? icon("check", size: "sm") : idx + 1 %>
</span>
<span><%= step[:name] %></span>

View file

@ -1,13 +1,15 @@
<%# locals: (budget:) %>
<div class="flex flex-col gap-4 items-center justify-center h-full">
<%= lucide_icon "alert-triangle", class: "w-6 h-6 text-red-500" %>
<%= icon "alert-triangle", size: "lg", color: "destructive" %>
<p class="text-secondary text-sm text-center">You have over-allocated your budget. Please fix your allocations.</p>
<%= link_to budget_budget_categories_path(budget), class: "btn btn--secondary flex items-center gap-1" do %>
<span class="text-primary font-medium">
Fix allocations
</span>
<%= lucide_icon "pencil", class: "w-4 h-4 text-secondary hover:text-gray-600" %>
<% end %>
<%= render LinkComponent.new(
text: "Fix allocations",
variant: "secondary",
size: "sm",
icon: "pencil",
icon_position: "right",
href: budget_budget_categories_path(budget)
) %>
</div>

View file

@ -1,17 +1,17 @@
<%# locals: (family:, year:) %>
<%= turbo_frame_tag "budget_picker" do %>
<div class="bg-container shadow-border-xs p-3 rounded-xl space-y-4">
<div class="p-3 space-y-4">
<div class="flex items-center gap-2 justify-between">
<% last_month_of_previous_year = Date.new(year - 1, 12, 1) %>
<% if Budget.budget_date_valid?(last_month_of_previous_year, family: family) %>
<%= link_to picker_budgets_path(year: year - 1), data: { turbo_frame: "budget_picker" }, class: "p-2 flex items-center justify-center hover:bg-alpha-black-25 rounded-md" do %>
<%= lucide_icon "chevron-left", class: "w-5 h-5 shrink-0 text-secondary" %>
<%= icon "chevron-left" %>
<% end %>
<% else %>
<span class="p-2 flex items-center justify-center text-gray-300 rounded-md">
<%= lucide_icon "chevron-left", class: "w-5 h-5 shrink-0 text-subdued" %>
<span class="p-2 flex items-center justify-center text-subdued rounded-md">
<%= icon "chevron-left", color: "current" %>
</span>
<% end %>
@ -23,11 +23,11 @@
<% if Budget.budget_date_valid?(first_month_of_next_year, family: family) %>
<%= link_to picker_budgets_path(year: year + 1), data: { turbo_frame: "budget_picker" }, class: "p-2 flex items-center justify-center hover:bg-alpha-black-25 rounded-md" do %>
<%= lucide_icon "chevron-right", class: "w-5 h-5 shrink-0 text-secondary" %>
<%= icon "chevron-right" %>
<% end %>
<% else %>
<span class="p-2 flex items-center justify-center text-gray-300 rounded-md">
<%= lucide_icon "chevron-right", class: "w-5 h-5 shrink-0 text-subdued" %>
<span class="p-2 flex items-center justify-center text-subdued rounded-md">
<%= icon "chevron-right", color: "current" %>
</span>
<% end %>
</div>
@ -38,7 +38,13 @@
<% param_key = Budget.date_to_param(date) %>
<% if Budget.budget_date_valid?(date, family: family) %>
<%= link_to month_name, budget_path(param_key), data: { turbo_frame: "_top" }, class: "btn btn--ghost" %>
<%= render LinkComponent.new(
variant: "ghost",
text: month_name,
href: budget_path(param_key),
full_width: true,
frame: :_top
) %>
<% else %>
<span class="px-3 py-2 text-subdued rounded-md"><%= month_name %></span>
<% end %>

View file

@ -21,7 +21,7 @@
<% if @budget.estimated_income && @budget.estimated_spending %>
<div class="border border-tertiary rounded-lg p-3 flex">
<%= lucide_icon "sparkles", class: "w-5 h-5 text-secondary shrink-0" %>
<%= icon "sparkles" %>
<div class="ml-2 space-y-1 text-sm">
<h4 class="text-primary">Autosuggest income & spending budget</h4>
<p class="text-secondary">
@ -29,18 +29,18 @@
</p>
</div>
<div class="relative inline-block select-none ml-6">
<%= check_box_tag :auto_fill, "1", params[:auto_fill].present?, class: "sr-only peer", data: {
action: "change->budget-form#toggleAutoFill",
budget_form_income_param: { key: "budget_expected_income", value: sprintf("%.2f", @budget.estimated_income) },
budget_form_spending_param: { key: "budget_budgeted_spending", value: sprintf("%.2f", @budget.estimated_spending) }
} %>
<label for="auto_fill" class="switch"></label>
</div>
<%= render ToggleComponent.new(
id: "auto_fill",
data: {
action: "change->budget-form#toggleAutoFill",
budget_form_income_param: { key: "budget_expected_income", value: sprintf("%.2f", @budget.estimated_income) },
budget_form_spending_param: { key: "budget_budgeted_spending", value: sprintf("%.2f", @budget.estimated_spending) }
}
) %>
</div>
<% end %>
<%= f.submit "Continue", class: "btn btn--primary w-full" %>
<%= f.submit "Continue" %>
<% end %>
</div>
</div>

View file

@ -54,10 +54,12 @@
<h2 class="text-lg font-medium">Categories</h2>
<% if @budget.initialized? %>
<%= link_to budget_budget_categories_path(@budget), class: "btn btn--secondary flex items-center gap-2" do %>
<%= icon "settings-2", color: "gray" %>
<span>Edit</span>
<% end %>
<%= render LinkComponent.new(
text: "Edit",
variant: "secondary",
icon: "settings-2",
href: budget_budget_categories_path(@budget)
) %>
<% end %>
</div>

View file

@ -8,9 +8,8 @@
border-color: color-mix(in srgb, <%= category.color %> 30%, white);
color: <%= category.color %>;">
<% if category.lucide_icon.present? %>
<%= lucide_icon category.lucide_icon, class: "w-4 h-4 shrink-0" %>
<%= icon category.lucide_icon, size: "sm", color: "current" %>
<% end %>
<%= category.name %>
</span>
</div>

View file

@ -3,24 +3,22 @@
<div id="<%= dom_id(category) %>" class="flex justify-between items-center px-4 pb-4 <%= "pt-4" unless category.subcategory? %> <%= "pb-4" unless category.subcategories.any? %> bg-container">
<div class="flex w-full items-center gap-2.5">
<% if category.subcategory? %>
<%= lucide_icon "corner-down-right", class: "shrink-0 w-5 h-5 text-subdued ml-2" %>
<span style="color: <%= category.color %>">
<%= icon "corner-down-right", size: "sm", color: "current", class: "ml-2" %>
</span>
<% end %>
<%= render partial: "categories/badge", locals: { category: category } %>
</div>
<div class="justify-self-end">
<%= contextual_menu do %>
<%= contextual_menu_modal_action_item t(".edit"), edit_category_path(category) %>
<%= render MenuComponent.new do |menu| %>
<% menu.with_item(variant: "link", text: t(".edit"), icon: "pencil", href: edit_category_path(category), data: { turbo_frame: :modal }) %>
<% if category.transactions.any? %>
<%= link_to new_category_deletion_path(category),
class: "flex items-center w-full rounded-lg text-red-600 hover:bg-red-50 py-2 px-3 gap-2",
data: { turbo_frame: :modal } do %>
<%= lucide_icon "trash-2", class: "shrink-0 w-5 h-5" %>
<span class="text-sm"><%= t(".delete") %></span>
<% end %>
<% menu.with_item(variant: "link", text: t(".delete"), icon: "trash-2", href: new_category_deletion_path(category), destructive: true, data: { turbo_frame: :modal }) %>
<% else %>
<%= contextual_menu_destructive_item t(".delete"), category_path(category), turbo_confirm: nil %>
<% menu.with_item(variant: "button", text: t(".delete"), icon: "trash-2", href: category_path(category), method: :delete) %>
<% end %>
<% end %>
</div>

View file

@ -4,5 +4,5 @@
data-category-target="avatar"
class="w-14 h-14 flex items-center justify-center rounded-full"
style="background-color: color-mix(in oklab, <%= category.color %> 10%, transparent); color: <%= category.color %>">
<%= lucide_icon(category.lucide_icon, class: "w-8 h-8") %>
<%= icon(category.lucide_icon, size: "2xl", color: "current") %>
</span>

View file

@ -2,59 +2,60 @@
<div data-controller="category" data-category-preset-colors-value="<%= Category::COLORS %>">
<%= styled_form_with model: category, class: "space-y-4" do |f| %>
<section class="space-y-4">
<div class="w-fit m-auto">
<section class="space-y-4 ">
<div class="w-fit mx-auto relative">
<%= render partial: "color_avatar", locals: { category: category } %>
<details data-category-target="details">
<summary class="cursor-pointer absolute -bottom-2 -right-2 flex justify-center items-center bg-gray-50 border-2 w-7 h-7 border-white rounded-full text-gray-500">
<%= icon("pen", size: "sm") %>
</summary>
<div class="fixed ml-8 mt-2 z-50 bg-container p-4 border border-alpha-black-25 rounded-2xl shadow-xs h-fit">
<div class="flex gap-2 flex-col mb-4" data-category-target="selection" style="<%= "display:none;" if @category.subcategory? %>">
<div data-category-target="pickerSection"></div>
<h4 class="text-gray-500 text-sm">Color</h4>
<div class="flex gap-2 items-center" data-category-target="colorsSection">
<% Category::COLORS.each do |color| %>
<label class="relative">
<%= f.radio_button :color, color, class: "sr-only peer", data: { action: "change->category#handleColorChange" } %>
<div class="w-6 h-6 rounded-full cursor-pointer peer-checked:ring-2 peer-checked:ring-offset-2 peer-checked:ring-blue-500" style="background-color: <%= color %>"></div>
</label>
<% end %>
<label class="relative">
<%= f.radio_button :color, "custom-color", class: "sr-only peer", data: { category_target: "colorPickerRadioBtn"} %>
<div class="w-6 h-6 rounded-full cursor-pointer peer-checked:ring-2 peer-checked:ring-offset-2 peer-checked:ring-blue-500" data-category-target="pickerBtn" style="background: conic-gradient(red,orange,yellow,lime,green,teal,cyan,blue,indigo,purple,magenta,pink,red)"></div>
</label>
</div>
<div class="flex gap-2 items-center hidden flex-col" data-category-target="paletteSection">
<div class="flex gap-2 items-center w-full">
<div class="w-6 h-6 p-4 rounded-full cursor-pointer" style="background-color: <%= category.color %>" data-category-target="colorPreview"></div>
<%= f.text_field :color , data: { category_target: "colorInput"}, inline: true %>
<%= icon "palette", size: "2xl", data: { action: "click->category#toggleSections" } %>
</div>
<div data-category-target="validationMessage" class="hidden self-start flex gap-1 items-center text-xs text-destructive ">
<span>Poor contrast, choose darker color or</span>
<button type="button" class="underline cursor-pointer" data-action="category#autoAdjust">auto-adjust.</button>
</div>
</div>
</div>
<div class="flex flex-wrap gap-2 justify-center flex-col w-87">
<h4 class="text-gray-500 text-sm">Icon</h4>
<div class="flex flex-wrap gap-0.5">
<% Category.icon_codes.each do |icon| %>
<label class="relative">
<%= f.radio_button :lucide_icon, icon, class: "sr-only peer", data: { action: "change->category#handleIconChange change->category#handleIconColorChange", category_target:"icon" } %>
<div class="w-7 h-7 flex m-0.5 items-center justify-center rounded-full cursor-pointer hover:bg-gray-100 peer-checked:bg-gray-100 border-1 border-transparent">
<%= icon(icon, size: "sm", color: "current") %>
</div>
</label>
<% end %>
</div>
</div>
</div>
</details>
</div>
<details data-category-target="details">
<summary class="cursor-pointer absolute top-23 left-58.5 flex justify-center items-center bg-gray-50 border-2 w-7 h-7 border-white rounded-full text-gray-500">
<%= icon("pen", size: "sm") %>
</summary>
<div class=" absolute z-50 bg-container p-4 border border-alpha-black-25 rounded-2xl shadow-xs h-fit left-66 top-24">
<div class="flex gap-2 flex-col mb-4" data-category-target="selection" style="<%= "display:none;" if @category.subcategory? %>">
<div data-category-target="pickerSection"></div>
<h4 class="text-gray-500 text-sm">Color</h4>
<div class="flex gap-2 items-center" data-category-target="colorsSection">
<% Category::COLORS.each do |color| %>
<label class="relative">
<%= f.radio_button :color, color, class: "sr-only peer", data: { action: "change->category#handleColorChange" } %>
<div class="w-6 h-6 rounded-full cursor-pointer peer-checked:ring-2 peer-checked:ring-offset-2 peer-checked:ring-blue-500" style="background-color: <%= color %>"></div>
</label>
<% end %>
<label class="relative">
<%= f.radio_button :color, "custom-color", class: "sr-only peer", data: { category_target: "colorPickerRadioBtn"} %>
<div class="w-6 h-6 rounded-full cursor-pointer peer-checked:ring-2 peer-checked:ring-offset-2 peer-checked:ring-blue-500" data-category-target="pickerBtn" style="background: conic-gradient(red,orange,yellow,lime,green,teal,cyan,blue,indigo,purple,magenta,pink,red)"></div>
</label>
</div>
<div class="flex gap-2 items-center hidden flex-col" data-category-target="paletteSection">
<div class="flex gap-2 items-center w-full">
<div class="w-6 h-6 p-4 rounded-full cursor-pointer" style="background-color: <%= category.color %>" data-category-target="colorPreview"></div>
<%= f.text_field :color , data: { category_target: "colorInput"}, inline: true %>
<%= lucide_icon "palette", class: "w-8 h-8 cursor-pointer hover:bg-gray-100 p-1", data: { action: "click->category#toggleSections" } %>
</div>
<div data-category-target="validationMessage" class="hidden self-start flex gap-1 items-center text-xs text-destructive ">
<span>Poor contrast, choose darker color or</span>
<button type="button" class="underline cursor-pointer" data-action="category#autoAdjust">auto-adjust.</button>
</div>
</div>
</div>
<div class="flex flex-wrap gap-2 justify-center flex-col w-87">
<h4 class="text-gray-500 text-sm">Icon</h4>
<div class="flex flex-wrap gap-0.5">
<% Category.icon_codes.each do |icon| %>
<label class="relative">
<%= f.radio_button :lucide_icon, icon, class: "sr-only peer", data: { action: "change->category#handleIconChange change->category#handleIconColorChange", category_target:"icon" } %>
<div class="w-7 h-7 flex m-0.5 items-center justify-center rounded-full cursor-pointer hover:bg-gray-100 peer-checked:bg-gray-100 border-1 border-transparent">
<%= lucide_icon icon, class: "w-6 h-6 p-1" %>
</div>
</label>
<% end %>
</div>
</div>
</div>
</details>
<% if category.errors.any? %>
<%= render "shared/form_errors", model: category %>

View file

@ -1,16 +1,15 @@
<%# locals: (transaction:) %>
<div class="relative" data-controller="menu" id="<%= dom_id(transaction, :category_menu) %>">
<button data-menu-target="button" class="flex cursor-pointer">
<%= render partial: "categories/badge", locals: { category: transaction.category } %>
</button>
<div data-menu-target="content" class="absolute z-10 hidden w-screen mt-2 max-w-min cursor-default">
<div class="w-80 text-sm font-semibold leading-6 text-primary bg-container shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
<%= turbo_frame_tag "category_dropdown", src: category_dropdown_path(category_id: transaction.category_id, transaction_id: transaction.id), loading: :lazy do %>
<div class="p-6 flex items-center justify-center">
<p class="text-sm text-secondary animate-pulse"><%= t(".loading") %></p>
</div>
<% end %>
</div>
</div>
</div>
<%= render MenuComponent.new(variant: "button") do |menu| %>
<% menu.with_button do %>
<% render partial: "categories/badge", locals: { category: transaction.category } %>
<% end %>
<% menu.with_custom_content do %>
<%= turbo_frame_tag "category_dropdown", src: category_dropdown_path(category_id: transaction.category_id, transaction_id: transaction.id), loading: :lazy do %>
<div class="p-6 flex items-center justify-center">
<p class="text-sm text-secondary animate-pulse"><%= t(".loading") %></p>
</div>
<% end %>
<% end %>
<% end %>

View file

@ -1,3 +1,6 @@
<%= modal_form_wrapper title: t(".edit"), overflow_visible: true do %>
<%= render "form", category: @category, categories: @categories %>
<%= render DialogComponent.new do |dialog| %>
<% dialog.with_header(title: t(".edit")) %>
<% dialog.with_body do %>
<%= render "form", category: @category, categories: @categories %>
<% end %>
<% end %>

View file

@ -2,18 +2,23 @@
<h1 class="text-primary text-xl font-medium"><%= t(".categories") %></h1>
<div class="flex items-center gap-2">
<%= contextual_menu do %>
<%= contextual_menu_destructive_item "Delete all", destroy_all_categories_path, turbo_confirm: {
title: "Delete all categories?",
body: "All of your transactions will become uncategorized and this cannot be undone.",
accept: "Delete all categories",
} %>
<%= render MenuComponent.new do |menu| %>
<% menu.with_item(
variant: "button",
text: "Delete all",
href: destroy_all_categories_path,
method: :delete,
icon: "trash-2",
confirm: CustomConfirm.for_resource_deletion("All categories", high_severity: true)) %>
<% end %>
<%= link_to new_category_path, class: "btn btn--primary flex items-center gap-1 justify-center", data: { turbo_frame: :modal } do %>
<%= lucide_icon "plus", class: "w-5 h-5" %>
<p><%= t(".new") %></p>
<% end %>
<%= render LinkComponent.new(
text: t(".new"),
variant: "primary",
icon: "plus",
href: new_category_path,
frame: :modal
) %>
</div>
</header>
@ -33,12 +38,18 @@
<div class="text-center flex flex-col items-center max-w-[500px]">
<p class="text-sm text-secondary mb-4"><%= t(".empty") %></p>
<div class="flex items-center gap-2">
<%= button_to t(".bootstrap"), bootstrap_categories_path, class: "btn btn--primary" %>
<%= render ButtonComponent.new(
text: t(".bootstrap"),
href: bootstrap_categories_path,
) %>
<%= link_to new_category_path, class: "btn btn--outline flex items-center gap-1", data: { turbo_frame: "modal" } do %>
<%= lucide_icon("plus", class: "w-5 h-5") %>
<span><%= t(".new") %></span>
<% end %>
<%= render LinkComponent.new(
text: t(".new"),
variant: "outline",
icon: "plus",
href: new_category_path,
frame: :modal
) %>
</div>
</div>
</div>

View file

@ -1,3 +1,6 @@
<%= modal_form_wrapper title: t(".new_category"), overflow_visible: true do %>
<%= render "form", category: @category, categories: @categories %>
<%= render DialogComponent.new do |dialog| %>
<% dialog.with_header(title: t(".new_category")) %>
<% dialog.with_body do %>
<%= render "form", category: @category, categories: @categories %>
<% end %>
<% end %>

View file

@ -1,21 +1,32 @@
<%= modal_form_wrapper title: t(".delete_category"), subtitle: t(".explanation", category_name: @category.name) do %>
<%= styled_form_with url: category_deletions_path(@category),
class: "space-y-4",
<%= render DialogComponent.new do |dialog| %>
<% dialog.with_header(title: t(".delete_category"), subtitle: t(".explanation", category_name: @category.name)) %>
<% dialog.with_body do %>
<%= styled_form_with url: category_deletions_path(@category),
data: {
turbo: false,
controller: "deletion",
deletion_dangerous_action_class: "form-field__submit bg-container text-red-600 border hover:bg-red-50",
deletion_safe_action_class: "form-field__submit border border-transparent",
deletion_submit_text_when_not_replacing_value: t(".delete_and_leave_uncategorized", category_name: @category.name),
deletion_submit_text_when_replacing_value: t(".delete_and_recategorize", category_name: @category.name) } do |f| %>
<%= f.collection_select :replacement_category_id,
<%= f.collection_select :replacement_category_id,
Current.family.categories.alphabetically.without(@category),
:id, :name,
{ prompt: t(".replacement_category_prompt"), label: t(".category") },
{ data: { deletion_target: "replacementField", action: "deletion#updateSubmitButton" } } %>
{ prompt: t(".replacement_category_prompt"), label: t(".category"), container_class: "mb-4" },
data: { deletion_target: "replacementField", action: "deletion#chooseSubmitButton" } %>
<%= f.submit t(".delete_and_leave_uncategorized", category_name: @category.name),
class: "form-field__submit bg-container text-red-600 border hover:bg-red-50",
data: { deletion_target: "submitButton" } %>
<%= render ButtonComponent.new(
variant: "destructive",
text: t(".delete_and_leave_uncategorized", category_name: @category.name),
full_width: true,
data: { deletion_target: "destructiveSubmitButton" }
) %>
<%= render ButtonComponent.new(
text: "Delete and reassign",
data: { deletion_target: "safeSubmitButton" },
hidden: true,
full_width: true
) %>
<% end %>
<% end %>
<% end %>

View file

@ -15,32 +15,17 @@
method: :patch,
class: "flex w-full items-center gap-1.5 cursor-pointer focus:outline-none" do %>
<span class="w-5 h-5">
<%= lucide_icon("check", class: "w-5 h-5 text-secondary") if is_selected %>
</span>
<%= icon("check") if is_selected %>
<% if category.subcategory? %>
<%= lucide_icon "corner-down-right", class: "shrink-0 w-5 h-5 text-subdued" %>
<%= icon("corner-down-right", size: "sm") %>
<% end %>
<%= render partial: "categories/badge", locals: { category: category } %>
<% end %>
<%= contextual_menu do %>
<div class="w-48 p-1 text-sm leading-6 text-primary bg-container shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
<%= link_to edit_category_path(category),
class: "block w-full py-2 px-3 space-x-2 text-primary hover:bg-gray-50 flex items-center rounded-lg",
data: { turbo_frame: :modal } do %>
<%= lucide_icon "pencil-line", class: "w-5 h-5 text-secondary" %>
<span><%= t(".edit") %></span>
<% end %>
<%= link_to new_category_deletion_path(category),
class: "block w-full py-2 px-3 space-x-2 text-red-600 hover:bg-red-50 flex items-center rounded-lg",
data: { turbo_frame: :modal } do %>
<%= lucide_icon "trash-2", class: "w-5 h-5" %>
<span><%= t(".delete") %></span>
<% end %>
</div>
<%= render MenuComponent.new do |menu| %>
<% 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

@ -3,7 +3,7 @@
<div class="grow p-1.5">
<div class="relative flex items-center bg-container border border-secondary rounded-lg">
<input placeholder="<%= t(".search_placeholder") %>" autocomplete="nope" type="search" class="placeholder:text-sm placeholder:text-secondary font-normal h-10 relative pl-10 w-full border-none rounded-lg" data-list-filter-target="input" data-action="list-filter#filter">
<%= lucide_icon("search", class: "w-5 h-5 text-secondary ml-2 absolute inset-0 transform top-1/2 -translate-y-1/2") %>
<%= icon("search", class: "absolute inset-0 ml-2 transform top-1/2 -translate-y-1/2") %>
</div>
</div>
<div data-list-filter-target="list" class="flex flex-col gap-0.5 p-1.5 mt-0.5 mr-2 max-h-64 overflow-y-scroll scrollbar">
@ -22,7 +22,13 @@
<div class="flex justify-center items-center py-12">
<div class="text-center flex flex-col items-center max-w-[500px]">
<p class="text-sm text-secondary font-normal mb-4"><%= t(".empty") %></p>
<%= button_to t(".bootstrap"), bootstrap_categories_path, class: "btn btn--outline", data: { turbo_frame: :_top } %>
<%= render ButtonComponent.new(
text: t(".bootstrap"),
variant: "outline",
href: bootstrap_categories_path,
method: :post,
data: { turbo_frame: :_top }) %>
</div>
</div>
<% end %>
@ -35,7 +41,7 @@
data: { turbo_frame: dom_id(@transaction.entry) },
params: { entry: { entryable_type: "Transaction", entryable_attributes: { id: @transaction.id, category_id: nil } } },
class: "flex text-sm font-medium items-center gap-2 text-secondary w-full rounded-lg p-2 hover:bg-gray-100" do %>
<%= lucide_icon "minus", class: "w-5 h-5" %>
<%= icon("minus") %>
<%= t(".clear") %>
<% end %>
@ -45,7 +51,7 @@
<%= link_to new_transaction_transfer_match_path(@transaction.entry),
class: "flex text-sm font-medium items-center gap-2 text-secondary w-full rounded-lg p-2 hover:bg-gray-100",
data: { turbo_frame: "modal" } do %>
<%= lucide_icon "refresh-cw", class: "w-5 h-5" %>
<%= icon("refresh-cw") %>
<p>Match transfer/payment</p>
<% end %>
@ -66,7 +72,9 @@
<p>One-time <%= @transaction.entry.amount.negative? ? "income" : "expense" %></p>
<%= lucide_icon "asterisk", class: "w-5 h-5 shrink-0 text-orange-500 ml-auto" %>
<span class="text-orange-500 ml-auto">
<%= icon("asterisk", color: "current") %>
</span>
</div>
</div>
</div>

View file

@ -1,33 +1,26 @@
<div class="flex flex-col items-center justify-start h-full p-6 text-center">
<div class="border border-secondary rounded-lg p-4 bg-container">
<div class="w-16 h-16 bg-surface-inset rounded-full flex items-center justify-center mx-auto mb-4">
<%= icon("sparkles") %>
</div>
<h3 class="text-lg font-medium text-primary mb-2">Enable Personal Finance AI</h3>
<p class="text-gray-600 mb-6 text-sm">
<% if Current.user.ai_available? %>
Our personal finance AI can help answer questions about your finances and provide insights based on your data.
To use this feature, you'll need to explicitly enable it.
<% else %>
To use the AI assistant, you need to set the <code class="bg-surface-inset px-1 py-0.5 rounded font-mono text-xs">OPENAI_ACCESS_TOKEN</code>
environment variable in your self-hosted instance.
<% end %>
</p>
<% unless self_hosted? %>
<p class="text-gray-600 mb-6 text-sm">
NOTE: During beta testing, we'll be monitoring usage and AI conversations to improve the assistant.
</p>
<% end %>
<% if Current.user.ai_available? %>
<%= form_with url: user_path(Current.user), method: :patch, class: "w-full", data: { turbo: false } do |form| %>
<%= form.hidden_field "user[ai_enabled]", value: true %>
<%= form.hidden_field "user[redirect_to]", value: "home" %>
<%= form.submit "Enable AI Assistant", class: "cursor-pointer hover:bg-black w-full py-2 px-4 bg-gray-800 text-white rounded-lg text-sm font-medium" %>
<% end %>
<% end %>
<div class="rounded-lg p-4 bg-container shadow-border-xs">
<div class="flex justify-center">
<%= render "chats/ai_avatar" %>
</div>
<h3 class="text-sm font-medium text-primary mb-1 -mt-2 text-center">Enable Maybe AI</h3>
<p class="text-gray-600 mb-4 text-sm text-center">
<% if Current.user.ai_available? %>
Maybe AI can answer financial questions and provide insights based on your data. To use this feature you'll need to explicitly enable it.
<% else %>
To use the AI assistant, you need to set the <code class="bg-surface-inset px-1 py-0.5 rounded font-mono text-xs">OPENAI_ACCESS_TOKEN</code>
environment variable in your self-hosted instance.
<% end %>
</p>
<% if Current.user.ai_available? %>
<%= form_with url: user_path(Current.user), method: :patch, class: "w-full", data: { turbo: false } do |form| %>
<%= form.hidden_field "user[ai_enabled]", value: true %>
<%= form.hidden_field "user[redirect_to]", value: "home" %>
<%= form.submit "Enable Maybe AI", class: "cursor-pointer hover:bg-black w-full py-2 px-4 bg-gray-800 text-white rounded-lg text-sm font-medium" %>
<% end %>
<% end %>
<p class="text-xs text-secondary text-center mt-2">Disable anytime. All data sent to our LLM providers is anonymized.</p>
</div>

View file

@ -9,8 +9,14 @@
</p>
</div>
<%= contextual_menu icon: "more-vertical" do %>
<%= contextual_menu_item("Edit chat", url: edit_chat_path(chat), icon: "pencil", turbo_frame: dom_id(chat, :title)) %>
<%= contextual_menu_destructive_item("Delete chat", chat_path(chat)) %>
<%= render MenuComponent.new(icon_vertical: true) do |menu| %>
<% menu.with_item(variant: "link", text: "Edit chat", href: edit_chat_path(chat), icon: "pencil", frame: dom_id(chat, "title")) %>
<% menu.with_item(
variant: "button",
text: "Delete chat",
href: chat_path(chat),
icon: "trash-2",
method: :delete,
confirm: CustomConfirm.for_resource_deletion("Chat")) %>
<% end %>
<% end %>

View file

@ -13,12 +13,23 @@
</div>
</div>
<%= contextual_menu icon: "more-vertical", id: "chat-menu" do %>
<%= contextual_menu_item "Start new chat", url: new_chat_path, icon: "plus" %>
<%= render MenuComponent.new(icon_vertical: true) do |menu| %>
<% menu.with_item(variant: "link", text: "Start new chat", href: new_chat_path, icon: "plus") %>
<% unless chat.new_record? %>
<%= contextual_menu_item "Edit chat title", url: edit_chat_path(chat, ctx: "chat"), icon: "pencil", turbo_frame: dom_id(chat, "title") %>
<%= contextual_menu_destructive_item "Delete chat", chat_path(chat), turbo_confirm: "Are you sure you want to delete this chat?" %>
<% 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,
confirm: CustomConfirm.for_resource_deletion("Chat")) %>
<% end %>
<% end %>
</nav>

View file

@ -10,8 +10,9 @@
<div class="flex items-center justify-between gap-2">
<p class="text-xs text-red-500">Failed to generate response. Please try again.</p>
<%= button_to retry_chat_path(chat), method: :post, class: "btn btn--primary" do %>
<span>Retry</span>
<% end %>
<%= render ButtonComponent.new(
text: "Retry",
href: retry_chat_path(chat),
) %>
</div>
</div>

View file

@ -1,31 +1,30 @@
<%= turbo_frame_tag chat_frame do %>
<div class="flex flex-col h-full md:p-4">
<nav class="mb-6">
<% back_path = @last_viewed_chat ? chat_path(@last_viewed_chat) : new_chat_path %>
<%= link_to back_path, class: "w-9 h-9 flex items-center justify-center rounded-lg hover:bg-surface-hover" do %>
<%= icon("arrow-left", color: "gray" ) %>
<% end %>
</nav>
<div class="grow flex flex-col">
<h1 class="text-xl font-medium mb-6">Chats</h1>
<div data-controller="chat hotkey">
<%= turbo_frame_tag chat_frame do %>
<div class="flex flex-col h-full md:p-4">
<% if @chats.any? %>
<div class="space-y-2 px-0.5">
<%= render @chats %>
</div>
<% else %>
<div class="text-center py-12 bg-container rounded-lg border border-primary">
<div class="w-16 h-16 bg-surface-inset rounded-full flex items-center justify-center mx-auto mb-4">
<%= icon("message-square", size: "lg") %>
</div>
<h3 class="text-lg font-medium text-primary mb-1">No chats yet</h3>
<p class="text-gray-500 mb-4">Start a new conversation with the AI assistant</p>
</div>
<div class="mt-auto p-4 lg:mt-auto">
<%= render "messages/chat_form", chat: nil %>
</div>
<nav class="mb-6">
<% back_path = @last_viewed_chat ? chat_path(@last_viewed_chat) : new_chat_path %>
<%= link_to back_path, class: "w-9 h-9 flex items-center justify-center rounded-lg hover:bg-surface-hover" do %>
<%= icon("arrow-left", color: "gray" ) %>
<% end %>
</nav>
<% end %>
<div class="grow flex flex-col">
<% if @chats.any? %>
<h1 class="text-xl font-medium mb-6">Chats</h1>
<div class="space-y-2 px-0.5">
<%= render @chats %>
</div>
<% else %>
<h1 class="sr-only">Chats</h1>
<div class="mt-auto py-8">
<%= render "chats/ai_greeting" %>
</div>
<%= render "messages/chat_form" %>
<% end %>
</div>
</div>
</div>
<% end %>
<% end %>
</div>

View file

@ -1,35 +1,38 @@
<%= turbo_frame_tag chat_frame do %>
<%= turbo_stream_from @chat %>
<div data-controller="chat hotkey">
<%= turbo_frame_tag chat_frame do %>
<%= turbo_stream_from @chat %>
<h1 class="sr-only"><%= @chat.title %></h1>
<h1 class="sr-only"><%= @chat.title %></h1>
<div class="flex flex-col h-full">
<div class="md:p-4">
<%= render "chats/chat_nav", chat: @chat %>
</div>
<div class="flex flex-col h-full">
<div class="md:p-4">
<%= render "chats/chat_nav", chat: @chat %>
</div>
<div id="messages" class="grow overflow-y-auto p-4 space-y-6" data-chat-target="messages">
<% if @chat.conversation_messages.any? %>
<% @chat.conversation_messages.ordered.each do |message| %>
<%= render message %>
<div id="messages" class="grow overflow-y-auto p-4 space-y-6 pb-24 lg:pb-4" data-chat-target="messages">
<% if @chat.conversation_messages.any? %>
<% @chat.conversation_messages.ordered.each do |message| %>
<%= render message %>
<% end %>
<% else %>
<div class="mt-auto">
<%= render "chats/ai_greeting", context: "chat" %>
</div>
<% end %>
<% else %>
<div class="mt-auto">
<%= render "chats/ai_greeting", context: "chat" %>
</div>
<% end %>
<% if params[:thinking].present? %>
<%= render "chats/thinking_indicator", chat: @chat %>
<% end %>
<% if params[:thinking].present? %>
<%= render "chats/thinking_indicator", chat: @chat %>
<% end %>
<% if @chat.error.present? && @chat.needs_assistant_response? %>
<%= render "chats/error", chat: @chat %>
<% end %>
<% if @chat.error.present? && @chat.needs_assistant_response? %>
<%= render "chats/error", chat: @chat %>
<% end %>
</div>
<%# DESKTOP - Chat form %>
<div class="p-4 lg:mt-auto fixed lg:static left-0 bottom-16 w-full bg-surface">
<%= render "messages/chat_form", chat: @chat %>
</div>
</div>
<div class="p-4 lg:mt-auto">
<%= render "messages/chat_form", chat: @chat %>
</div>
</div>
<% end %>
<% end %>
</div>

View file

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

View file

@ -1,3 +1,7 @@
<%= modal_form_wrapper title: t(".edit", account: @account.name) do %>
<%= render "form", account: @account, url: credit_card_path(@account) %>
<%= render DialogComponent.new do |dialog| %>
<% dialog.with_header(title: t(".edit", account: @account.name)) %>
<% dialog.with_body do %>
<%= render "form", account: @account, url: credit_card_path(@account) %>
<% end %>
<% end %>

View file

@ -1,7 +1,10 @@
<% if params[:step] == "method_select" %>
<%= render "accounts/new/method_selector", path: new_credit_card_path(return_to: params[:return_to]), us_link_token: @us_link_token, eu_link_token: @eu_link_token %>
<% else %>
<%= modal_form_wrapper title: t(".title") do %>
<%= render "credit_cards/form", account: @account, url: credit_cards_path %>
<%= render DialogComponent.new do |dialog| %>
<% dialog.with_header(title: t(".title")) %>
<% dialog.with_body do %>
<%= render "credit_cards/form", account: @account, url: credit_cards_path %>
<% end %>
<% end %>
<% end %>

View file

@ -1,3 +1,6 @@
<%= modal_form_wrapper title: t(".edit", account: @account.name) do %>
<%= render "form", account: @account, url: crypto_path(@account) %>
<%= render DialogComponent.new do |dialog| %>
<% dialog.with_header(title: t(".edit", account: @account.name)) %>
<% dialog.with_body do %>
<%= render "form", account: @account, url: crypto_path(@account) %>
<% end %>
<% end %>

View file

@ -1,7 +1,10 @@
<% if params[:step] == "method_select" %>
<%= render "accounts/new/method_selector", path: new_crypto_path(return_to: params[:return_to]), us_link_token: @us_link_token, eu_link_token: @eu_link_token %>
<% else %>
<%= modal_form_wrapper title: t(".title") do %>
<%= render "cryptos/form", account: @account, url: cryptos_path %>
<%= render DialogComponent.new do |dialog| %>
<% dialog.with_header(title: t(".title")) %>
<% dialog.with_body do %>
<%= render "form", account: @account, url: cryptos_path %>
<% end %>
<% end %>
<% end %>

View file

@ -1,3 +1,6 @@
<%= modal_form_wrapper title: t(".edit", account: @account.name) do %>
<%= render "form", account: @account, url: depository_path(@account) %>
<%= render DialogComponent.new do |dialog| %>
<% dialog.with_header(title: t(".edit", account: @account.name)) %>
<% dialog.with_body do %>
<%= render "form", account: @account, url: depository_path(@account) %>
<% end %>
<% end %>

View file

@ -1,7 +1,10 @@
<% if params[:step] == "method_select" %>
<%= render "accounts/new/method_selector", path: new_depository_path(return_to: params[:return_to]), us_link_token: @us_link_token, eu_link_token: @eu_link_token %>
<% else %>
<%= modal_form_wrapper title: t(".title") do %>
<%= render "depositories/form", account: @account, url: depositories_path %>
<%= render DialogComponent.new do |dialog| %>
<% dialog.with_header(title: t(".title")) %>
<% dialog.with_body do %>
<%= render "depositories/form", account: @account, url: depositories_path %>
<% end %>
<% end %>
<% end %>

View file

@ -7,8 +7,8 @@
<div class="flex items-center gap-1 text-secondary">
<%= form_with url: transactions_bulk_deletion_path, data: { turbo_confirm: true, turbo_frame: "_top" } do %>
<button type="button" data-bulk-select-scope-param="bulk_delete" data-action="bulk-select#submitBulkRequest" class="p-1.5 group hover:bg-gray-700 flex items-center justify-center rounded-md" title="Delete">
<%= lucide_icon "trash-2", class: "w-5 group-hover:text-white" %>
<button type="button" data-bulk-select-scope-param="bulk_delete" data-action="bulk-select#submitBulkRequest" class="p-1.5 group hover:bg-inverse flex items-center justify-center rounded-md" title="Delete">
<%= icon "trash-2", class: "group-hover:text-inverse" %>
</button>
<% end %>
</div>

View file

@ -15,17 +15,15 @@
</p>
</div>
<div class="justify-self-end">
<%= contextual_menu do %>
<%= contextual_menu_modal_action_item t(".edit"), edit_family_merchant_path(family_merchant), icon: "pencil", turbo_frame: "modal" %>
<%= contextual_menu_destructive_item "Delete",
family_merchant_path(family_merchant),
turbo_frame: "_top",
turbo_confirm: family_merchant.transactions.any? ? {
title: "Delete #{family_merchant.name}?",
body: "This will remove this merchant from all transactions it has been assigned to.",
accept: "Delete"
} : nil %>
<%= render MenuComponent.new do |menu| %>
<% 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,
confirm: CustomConfirm.for_resource_deletion(family_merchant.name)) %>
<% end %>
</div>
</div>

View file

@ -1,11 +1,13 @@
<%# locals: (family_merchant:) %>
<div data-controller="color-avatar">
<%= styled_form_with model: @merchant, class: "space-y-4" do |f| %>
<%= styled_form_with model: family_merchant, class: "space-y-4" do |f| %>
<section class="space-y-4">
<% if @merchant.errors.any? %>
<%= render "shared/form_errors", model: @merchant %>
<% if family_merchant.errors.any? %>
<%= render "shared/form_errors", model: family_merchant %>
<% end %>
<div class="w-fit m-auto mb-4">
<%= render partial: "shared/color_avatar", locals: { name: @merchant.name, color: @merchant.color } %>
<%= render partial: "shared/color_avatar", locals: { name: family_merchant.name, color: family_merchant.color } %>
</div>
<div class="flex gap-2 items-center justify-center">
<% FamilyMerchant::COLORS.each do |color| %>

View file

@ -1,3 +1,6 @@
<%= modal_form_wrapper title: t(".title") do %>
<%= render "form", merchant: @merchant %>
<%= render DialogComponent.new do |dialog| %>
<% dialog.with_header(title: t(".title")) %>
<% dialog.with_body do %>
<%= render "form", family_merchant: @family_merchant %>
<% end %>
<% end %>

View file

@ -1,10 +1,12 @@
<header class="flex items-center justify-between">
<h1 class="text-primary text-xl font-medium">Merchants</h1>
<%= link_to new_family_merchant_path, class: "btn btn--primary flex items-center gap-2", data: { turbo_frame: "modal" } do %>
<%= lucide_icon("plus", class: "w-5 h-5") %>
<p class="text-sm font-medium">New merchant</p>
<% end %>
<%= render LinkComponent.new(
text: "New merchant",
variant: "primary",
href: new_family_merchant_path,
frame: :modal
) %>
</header>
<div class="bg-container shadow-border-xs rounded-xl p-4">
@ -26,10 +28,13 @@
<div class="flex justify-center items-center py-20">
<div class="text-center flex flex-col items-center max-w-[300px]">
<p class="text-primary mb-1 font-medium text-sm"><%= t(".empty") %></p>
<%= link_to new_family_merchant_path, class: "w-fit flex text-white text-sm font-medium items-center gap-1 bg-gray-900 rounded-lg p-2 pr-3", data: { turbo_frame: "modal" } do %>
<%= lucide_icon("plus", class: "w-5 h-5") %>
<span><%= t(".new") %></span>
<% end %>
<%= render LinkComponent.new(
text: t(".new"),
icon: "plus",
href: new_family_merchant_path,
frame: :modal
) %>
</div>
</div>
<% end %>

View file

@ -1,3 +1,6 @@
<%= modal_form_wrapper title: t(".title") do %>
<%= render "form", merchant: @merchant %>
<%= render DialogComponent.new do |dialog| %>
<% dialog.with_header(title: t(".title")) %>
<% dialog.with_body do %>
<%= render "form", family_merchant: @merchant %>
<% end %>
<% end %>

View file

@ -4,7 +4,12 @@
<div class="grid grid-cols-12 items-center text-primary text-sm font-medium p-4">
<div class="col-span-4 flex items-center gap-4">
<%= render "shared/circle_logo", name: currency.iso_code %>
<%= render FilledIconComponent.new(
variant: :text,
text: currency.symbol,
rounded: true,
size: "lg"
) %>
<div class="space-y-0.5">
<%= tag.p t(".brokerage_cash"), class: "text-primary" %>

View file

@ -1,6 +1,6 @@
<div data-controller="tooltip" data-tooltip-cross-axis-value="50">
<div class="flex items-center gap-1 text-warning">
<%= lucide_icon "info", class: "w-4 h-4 shrink-0" %>
<%= icon "info", size: "sm", color: "current" %>
<%= tag.span t(".missing_data"), class: "font-normal text-xs" %>
</div>
<div role="tooltip" data-tooltip-target="tooltip" class="tooltip bg-gray-700 text-sm p-2 rounded w-64">

View file

@ -6,7 +6,9 @@
id: dom_id(@account, "new_trade"),
data: { turbo_frame: :modal },
class: "flex gap-1 font-medium items-center bg-gray-50 text-primary p-2 rounded-lg" do %>
<%= lucide_icon("plus", class: "w-5 h-5 text-primary") %>
<span class="text-primary">
<%= icon("plus", color: "current") %>
</span>
<%= tag.span t(".new_holding"), class: "text-sm" %>
<% end %>
</div>

View file

@ -1,20 +1,17 @@
<%= drawer do %>
<div class="space-y-4">
<header class="flex justify-between">
<%= render DialogComponent.new(variant: "drawer") do |dialog| %>
<% dialog.with_header do %>
<div class="flex items-center justify-between">
<div>
<%= tag.h3 @holding.name, class: "text-2xl font-medium text-primary" %>
<%= tag.p @holding.ticker, class: "text-sm text-secondary" %>
</div>
<%= image_tag "https://logo.synthfinance.com/ticker/#{@holding.ticker}", loading: "lazy", class: "w-9 h-9 rounded-full" %>
</header>
<details class="group space-y-2" open>
<summary class="flex list-none items-center justify-between rounded-xl px-3 py-2 text-xs font-medium uppercase text-secondary bg-gray-25 focus-visible:outline-hidden">
<h4><%= t(".overview") %></h4>
<%= lucide_icon "chevron-down", class: "group-open:transform group-open:rotate-180 text-secondary w-5" %>
</summary>
</div>
<% end %>
<% dialog.with_body do %>
<% dialog.with_section(title: t(".overview"), open: true) do %>
<div class="pb-4">
<dl class="space-y-3 px-3 py-2">
<div class="flex items-center justify-between text-sm">
@ -45,14 +42,9 @@
</div>
</dl>
</div>
</details>
<details class="group space-y-2" open>
<summary class="flex list-none items-center justify-between rounded-xl px-3 py-2 text-xs font-medium uppercase text-secondary bg-gray-25 focus-visible:outline-hidden">
<h4><%= t(".history") %></h4>
<%= lucide_icon "chevron-down", class: "group-open:transform group-open:rotate-180 text-secondary w-5" %>
</summary>
<% end %>
<% dialog.with_section(title: t(".history"), open: true) do %>
<div class="space-y-2">
<div class="px-3 py-4">
<% if @holding.trades.any? %>
@ -85,15 +77,10 @@
<% end %>
</div>
</div>
</details>
<% end %>
<% unless @holding.account.plaid_account_id.present? %>
<details class="group space-y-2" open>
<summary class="flex list-none items-center justify-between rounded-xl px-3 py-2 text-xs font-medium uppercase text-secondary bg-gray-25 focus-visible:outline-hidden">
<h4><%= t(".settings") %></h4>
<%= lucide_icon "chevron-down", class: "group-open:transform group-open:rotate-180 text-secondary w-5" %>
</summary>
<% dialog.with_section(title: t(".settings"), open: true) do %>
<div class="pb-4">
<div class="flex items-center justify-between gap-2 p-3">
<div class="text-sm space-y-1">
@ -108,7 +95,7 @@
data: { turbo_confirm: true } %>
</div>
</div>
</details>
<% end %>
<% end %>
</div>
<% end %>
<% end %>

View file

@ -2,9 +2,9 @@
<% in_progress_session = Current.true_user.impersonated_support_sessions.in_progress.first %>
<div class="sticky top-0 left-0 w-full bg-black flex items-center justify-between font-mono">
<div class="flex items-center bg-red-600 px-6 py-4">
<%= lucide_icon "alert-triangle", class: "w-6 h-6 text-white mr-2" %>
<span class="text-white font-semibold uppercase">Access <%= in_progress_session.present? ? "Session" : "Request" %></span>
<div class="flex items-center bg-red-600 text-inverse px-6 py-4">
<%= icon "alert-triangle", size: "lg", color: "current", class: "mr-2" %>
<span class="text-inverse font-semibold uppercase">Access <%= in_progress_session.present? ? "Session" : "Request" %></span>
</div>
<div class="flex items-center space-x-2 px-2 py-2 text-white gap-4">
<% if pending_session.present? %>

View file

@ -1,7 +1,7 @@
<div class="sticky top-0 left-0 w-full bg-black flex items-center justify-between font-mono">
<div class="flex items-center bg-red-600 px-6 py-4">
<%= lucide_icon "alert-triangle", class: "w-6 h-6 text-white mr-2" %>
<span class="text-white font-semibold uppercase">Super Admin</span>
<%= icon "alert-triangle", size: "lg", color: "current", class: "mr-2" %>
<span class="text-inverse font-semibold uppercase">Super Admin</span>
</div>
<div>
<%= link_to "Jobs", sidekiq_web_url, class: "text-white underline hover:text-gray-100" %>

View file

@ -13,18 +13,24 @@
<% if @import.cleaned? %>
<div class="bg-container border border-tertiary rounded-lg p-3 flex flex-col md:flex-row items-start md:items-center justify-between gap-2 md:gap-0">
<div class="flex items-center gap-2">
<%= lucide_icon "check-circle", class: "w-4 h-4 text-green-500" %>
<p class="text-green-500 text-sm md:text-base">Your data has been cleaned</p>
<%= icon "check-circle", size: "sm", color: "success" %>
<p class="text-success text-sm md:text-base">Your data has been cleaned</p>
</div>
<%= link_to "Next step", import_confirm_path(@import), class: "btn btn--primary w-full md:w-auto" %>
<%= render LinkComponent.new(
text: "Next step",
variant: "primary",
href: import_confirm_path(@import),
frame: :_top,
class: "w-full md:w-auto"
) %>
</div>
<% else %>
<div class="bg-container border border-tertiary rounded-lg p-3 flex flex-col md:flex-row items-start md:items-center justify-between gap-3 md:gap-0">
<div class="flex items-center gap-2">
<%= lucide_icon "alert-triangle", class: "w-4 h-4 text-red-500 flex-shrink-0" %>
<p class="text-red-500 text-sm hidden md:block"><%= t(".errors_notice") %></p>
<p class="text-red-500 text-sm md:hidden"><%= t(".errors_notice_mobile") %></p>
<%= icon "alert-triangle", size: "sm", color: "destructive" %>
<p class="text-destructive text-sm hidden md:block"><%= t(".errors_notice") %></p>
<p class="text-destructive text-sm md:hidden"><%= t(".errors_notice_mobile") %></p>
</div>
<div class="flex justify-center w-full md:w-auto">

View file

@ -6,5 +6,5 @@
<%= form.select :amount_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Balance" }, required: true %>
<%= form.select :currency_col_label, import.csv_headers, { include_blank: "Default", label: "Currency" } %>
<%= form.submit "Apply configuration", class: "w-full btn btn--primary", disabled: import.complete? %>
<%= form.submit "Apply configuration", disabled: import.complete? %>
<% end %>

View file

@ -1,7 +1,9 @@
<%# locals: (import:) %>
<div class="flex items-center justify-between border border-secondary rounded-lg bg-green-500/5 p-5 gap-4 mb-4">
<%= lucide_icon("check-circle", class: "w-5 h-5 shrink-0 text-green-500") %>
<span class="text-green-500">
<%= icon("check-circle", color: "current") %>
</span>
<p class="text-sm text-primary italic">We have pre-configured your Mint import for you. Please proceed to the next step.</p>
</div>
@ -29,5 +31,5 @@
<%= form.select :category_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Category (optional)" }, disabled: import.complete? %>
<%= form.select :tags_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Tags (optional)" }, disabled: import.complete? %>
<%= form.submit "Apply configuration", class: "w-full btn btn--primary", disabled: import.complete? %>
<%= form.submit "Apply configuration", disabled: import.complete? %>
<% end %>

View file

@ -36,5 +36,5 @@
<% end %>
</div>
<%= form.submit "Apply configuration", class: "w-full btn btn--primary", disabled: import.complete? %>
<%= form.submit "Apply configuration", disabled: import.complete? %>
<% end %>

View file

@ -107,7 +107,5 @@
import.csv_headers,
{ include_blank: "Leave empty", label: "Notes" } %>
<%= form.submit "Apply configuration",
class: "w-full btn btn--primary",
disabled: import.complete? %>
<%= form.submit "Apply configuration", disabled: import.complete? %>
<% end %>

View file

@ -18,8 +18,8 @@
<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">
<%= link_to "Manually configure", import_configuration_path(@import), class: "btn btn--outline" %>
<%= button_to "Apply template", apply_template_import_path(@import), class: "btn btn--primary", method: :put, data: { turbo_frame: :_top } %>
<%= 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>
</div>

View file

@ -8,18 +8,29 @@
<% if import.requires_account? %>
<div class="w-full max-w-full overflow-hidden mb-4">
<div class="overflow-x-auto">
<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">
<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" %>
<%= link_to t(".create_account"), new_account_path(return_to: import_confirm_path(import)), class: "btn btn--primary whitespace-nowrap", data: { turbo_frame: :modal } %>
<%= render LinkComponent.new(
text: "Create account",
variant: "primary",
href: new_account_path(return_to: import_confirm_path(import)),
frame: :modal
) %>
</div>
</div>
</div>
<% elsif import.has_unassigned_account? %>
<div class="w-full max-w-full overflow-hidden mb-4">
<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">
<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" %>
<%= link_to t(".create_account"), new_account_path(return_to: import_confirm_path(import)), class: "btn btn--primary whitespace-nowrap", data: { turbo_frame: :modal } %>
<%= render LinkComponent.new(
text: t(".create_account"),
variant: "primary",
href: new_account_path(return_to: import_confirm_path(import)),
frame: :modal
) %>
</div>
</div>
</div>
@ -29,7 +40,7 @@
<div class="space-y-4 w-full max-w-full">
<div class="w-full max-w-full overflow-hidden">
<div class="overflow-x-auto">
<div class="bg-container-inset rounded-xl p-1 space-y-1 w-[650px] min-w-0">
<div class="bg-container-inset rounded-xl p-1 space-y-1 w-[650px] min-w-0 mx-auto">
<div class="grid grid-cols-3 gap-2 text-xs font-medium text-secondary uppercase px-5 py-3">
<p><%= t(".csv_mapping_label", mapping: mapping_label(mapping_class)) %></p>
<p><%= t(".maybe_mapping_label", mapping: mapping_label(mapping_class)) %></p>
@ -48,10 +59,14 @@
</div>
<div class="flex justify-center w-full">
<%= link_to is_last_step ? import_path(import) : url_for(step: step_idx + 2), class: "btn btn--primary w-full md:w-36 flex items-center justify-between gap-2" do %>
<span>Next</span>
<%= lucide_icon "arrow-right", class: "w-5 h-5" %>
<% end %>
<%= 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

@ -28,10 +28,10 @@
disabled: row.import.complete? %>
<% if !cell_is_valid?(row, key) %>
<span class="absolute right-2 top-1/2 -translate-y-1/2 text-red-500 md:hidden"
<span class="absolute right-2 top-1/2 -translate-y-1/2 text-destructive md:hidden"
data-action="click->mobile-cell-interaction#toggleErrorMessage"
data-mobile-cell-interaction-target="errorIcon">
<%= lucide_icon "alert-circle", class: "w-4 h-4" %>
data-mobile-cell-interaction-target="errorIcon">
<%= icon "alert-circle", size: "sm", color: "destructive" %>
</span>
<div class="absolute left-4 right-4 bottom-full mb-2 p-2 bg-red-50 border border-red-200 rounded-lg shadow-lg text-xs text-red-600 hidden md:hidden z-20"

View file

@ -11,65 +11,67 @@
<p class="text-secondary text-sm"><%= t(".description") %></p>
</div>
<div
data-controller="tabs"
data-tabs-active-class="bg-surface shadow-sm text-primary"
data-tabs-inactive-class="text-secondary"
data-tabs-default-tab-value="csv-upload-tab">
<div class="flex justify-center mb-4 w-full">
<div class="bg-surface-inset rounded-lg p-1 flex w-full">
<button type="button" data-id="csv-upload-tab" class="w-1/2 px-2 py-1 rounded-md text-secondary text-sm font-medium" data-tabs-target="btn" data-action="click->tabs#select">Upload CSV</button>
<button type="button" data-id="csv-paste-tab" class="w-1/2 px-2 py-1 rounded-md text-sm text-secondary font-medium" data-tabs-target="btn" data-action="click->tabs#select">Copy & Paste</button>
</div>
</div>
<%= render TabsComponent.new(active_tab: params[:tab] || "csv-upload", url_param_key: "tab", testid: "import-tabs") do |tabs| %>
<% tabs.with_nav do |nav| %>
<% nav.with_btn(id: "csv-upload", label: "Upload CSV") %>
<% nav.with_btn(id: "csv-paste", label: "Copy & Paste") %>
<% end %>
<% ["csv-paste-tab", "csv-upload-tab"].each do |tab| %>
<%= tag.div id: tab, data: { tabs_target: "tab" }, class: tab == "csv-upload-tab" ? "hidden" : "" do %>
<%= styled_form_with model: @import, scope: :import, url: import_upload_path(@import), multipart: true, class: "space-y-2" do |form| %>
<%= form.select :col_sep, Import::SEPARATORS, label: true %>
<% tabs.with_panel(tab_id: "csv-upload") do %>
<%= styled_form_with model: @import, scope: :import, url: import_upload_path(@import), multipart: true, class: "space-y-2" do |form| %>
<%= form.select :col_sep, Import::SEPARATORS, label: true %>
<% if @import.type == "TransactionImport" || @import.type == "TradeImport" %>
<%= form.select :account_id, @import.family.accounts.pluck(:name, :id), { label: "Account (optional)", include_blank: "Multi-account import", selected: @import.account_id } %>
<% end %>
<% if @import.type == "TransactionImport" || @import.type == "TradeImport" %>
<%= form.select :account_id, @import.family.accounts.pluck(:name, :id), { label: "Account (optional)", include_blank: "Multi-account import", selected: @import.account_id } %>
<% end %>
<% if tab == "csv-paste-tab" %>
<%= form.text_area :raw_file_str,
<div class="flex flex-col items-center justify-center w-full h-64 border border-secondary border-dashed rounded-xl cursor-pointer" data-controller="file-upload" data-action="click->file-upload#triggerFileInput" data-file-upload-target="uploadArea">
<div class="flex flex-col items-center justify-center pt-5 pb-6">
<div data-file-upload-target="uploadText" class="flex flex-col items-center">
<%= icon("plus", size: "lg", class: "mb-4 mx-auto") %>
<p class="mb-2 text-md text-gray text-center">
<span class="font-medium text-primary">Browse</span> to add your CSV file here
</p>
</div>
<div class="flex flex-col gap-4 items-center hidden mb-2" data-file-upload-target="fileName">
<span class="text-primary">
<%= icon("file-text", size: "lg", color: "current") %>
</span>
<p class="text-md font-medium text-primary"></p>
</div>
<%= form.file_field :csv_file, class: "hidden", "data-auto-submit-form-target": "auto", "data-file-upload-target": "input" %>
</div>
</div>
<%= form.submit "Upload CSV", disabled: @import.complete? %>
<% end %>
<% end %>
<% tabs.with_panel(tab_id: "csv-paste") do %>
<%= styled_form_with model: @import, scope: :import, url: import_upload_path(@import), multipart: true, class: "space-y-2" do |form| %>
<%= form.select :col_sep, Import::SEPARATORS, label: true %>
<% if @import.type == "TransactionImport" || @import.type == "TradeImport" %>
<%= form.select :account_id, @import.family.accounts.pluck(:name, :id), { label: "Account (optional)", include_blank: "Multi-account import", selected: @import.account_id } %>
<% end %>
<%= form.text_area :raw_file_str,
rows: 10,
required: true,
placeholder: "Paste your CSV file contents here",
"data-auto-submit-form-target": "auto" %>
<% else %>
<div class="flex flex-col items-center justify-center w-full h-64 border border-secondary border-dashed rounded-xl cursor-pointer" data-controller="file-upload" data-action="click->file-upload#triggerFileInput" data-file-upload-target="uploadArea">
<div class="flex flex-col items-center justify-center pt-5 pb-6">
<div data-file-upload-target="uploadText" class="flex flex-col items-center">
<%= lucide_icon("plus", class: "w-6 h-6 mb-4 text-secondary mx-auto") %>
<p class="mb-2 text-md text-gray text-center">
<span class="font-medium text-primary">Browse</span> to add your CSV file here
</p>
</div>
<div class="flex flex-col items-center hidden" data-file-upload-target="fileName">
<%= lucide_icon("file-text", class: "w-6 h-6 mb-4 text-primary") %>
<p class="mb-2 text-md font-medium text-primary"></p>
</div>
<%= form.file_field :csv_file, class: "hidden", "data-auto-submit-form-target": "auto", "data-file-upload-target": "input" %>
</div>
</div>
<% end %>
<%= form.submit "Upload CSV", disabled: @import.complete? %>
<% end %>
<%= form.submit "Upload CSV", disabled: @import.complete? %>
<% end %>
<% end %>
</div>
<% end %>
</div>
<div class="flex justify-center">
<span class="text-secondary text-sm">
<%= link_to "Download a sample CSV", "/imports/#{@import.id}/upload/sample_csv", class: "text-primary underline", data: { turbo: false } %> to see the required CSV format
</span>
</div>
<div class="flex justify-center">
<span class="text-secondary text-sm">
<%= link_to "Download a sample CSV", "/imports/#{@import.id}/upload/sample_csv", class: "text-primary underline", data: { turbo: false } %> to see the required CSV format
</span>
</div>
</div>

View file

@ -1,9 +1,13 @@
<div class="flex justify-center items-center py-20">
<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>
<%= link_to new_import_path, class: "btn btn--primary flex items-center gap-2", data: { turbo_frame: "modal" } do %>
<%= lucide_icon("plus", class: "w-5 h-5") %>
<span><%= t(".new") %></span>
<% end %>
<%= render LinkComponent.new(
text: t(".new"),
variant: "primary",
href: new_import_path,
icon: "plus",
frame: :modal
) %>
</div>
</div>

View file

@ -3,7 +3,7 @@
<div class="h-full flex flex-col justify-center items-center">
<div class="space-y-6 max-w-sm">
<div class="mx-auto bg-red-500/5 h-8 w-8 rounded-full flex items-center justify-center">
<%= lucide_icon "alert-octagon", class: "w-5 h-5 text-red-500" %>
<%= icon "alert-octagon", color: "destructive" %>
</div>
<div class="text-center space-y-2">
@ -11,8 +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>
<div>
<%= button_to "Try again", publish_import_path(import), class: "btn btn--primary text-center w-full" %>
</div>
<%= render ButtonComponent.new(text: "Try again", href: publish_import_path(import), full_width: true) %>
</div>
</div>

View file

@ -36,34 +36,30 @@
<% end %>
</div>
<%= contextual_menu do %>
<div class="w-48 p-1 text-sm leading-6 text-primary bg-container shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
<%= link_to import_path(import),
class: "block w-full py-2 px-3 space-x-2 text-primary hover:bg-gray-50 flex items-center rounded-lg" do %>
<%= lucide_icon "eye", class: "w-5 h-5 text-secondary" %>
<%= render MenuComponent.new do |menu| %>
<% menu.with_item(variant: "link", text: t(".view"), href: import_path(import), icon: "eye") %>
<span><%= t(".view") %></span>
<% end %>
<% if import.complete? || import.revert_failed? %>
<% menu.with_item(
variant: "button",
text: t(".revert"),
href: revert_import_path(import),
icon: "rotate-ccw",
method: :put,
confirm: CustomConfirm.new(
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.",
btn_text: "Revert"
)) %>
<% if import.complete? || import.revert_failed? %>
<%= button_to revert_import_path(import),
method: :put,
class: "block w-full py-2 px-3 space-x-2 text-orange-600 hover:bg-orange-50 flex items-center rounded-lg",
data: { turbo_confirm: true } do %>
<%= lucide_icon "rotate-ccw", class: "w-5 h-5" %>
<span>Revert</span>
<% end %>
<% else %>
<%= button_to import_path(import),
method: :delete,
class: "block w-full py-2 px-3 space-x-2 text-red-600 hover:bg-red-50 flex items-center rounded-lg",
data: { turbo_confirm: true } do %>
<%= lucide_icon "trash-2", class: "w-5 h-5" %>
<span><%= t(".delete") %></span>
<% end %>
<% end %>
</div>
<% else %>
<% menu.with_item(
variant: "button",
text: t(".delete"),
href: import_path(import),
icon: "trash-2",
method: :delete,
confirm: CustomConfirm.for_resource_deletion("Import")) %>
<% end %>
<% end %>
</div>

View file

@ -3,7 +3,7 @@
<div class="h-full flex flex-col justify-center items-center">
<div class="space-y-6 max-w-sm">
<div class="mx-auto bg-gray-500/5 h-8 w-8 rounded-full flex items-center justify-center">
<%= lucide_icon "loader", class: "animate-pulse w-5 h-5 text-secondary" %>
<%= icon "loader", class: "animate-pulse" %>
</div>
<div class="text-center space-y-2">
@ -12,8 +12,8 @@
</div>
<div class="space-y-2">
<%= link_to "Check status", import_path(import), class: "block btn btn--primary text-center w-full" %>
<%= link_to "Back to dashboard", root_path, class: "block btn btn--secondary text-center w-full" %>
<%= 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

@ -36,7 +36,7 @@
<%= link_to step[:path], class: "flex items-center gap-3" do %>
<div class="flex items-center gap-2 text-sm font-medium <%= text_class %>">
<span class="<%= step_class %> w-7 h-7 rounded-full shrink-0 inline-flex items-center justify-center border border-transparent">
<%= step[:is_complete] && !is_current ? lucide_icon("check", class: "w-4 h-4") : idx + 1 %>
<%= step[:is_complete] && !is_current ? icon("check", size: "sm") : idx + 1 %>
</span>
<span><%= step[:name] %></span>

View file

@ -18,9 +18,9 @@
<div class="flex items-center justify-between gap-2 bg-container px-5 py-3 rounded-tl-lg rounded-tr-lg">
<div class="flex items-center gap-3">
<div class="<%= resource.bg_class %> w-8 h-8 rounded-full flex justify-center items-center">
<%= lucide_icon resource.icon, class: "#{resource.text_class} w-5 h-5 shrink-0" %>
</div>
<%= tag.div class: class_names(resource.bg_class, resource.text_class, "w-8 h-8 rounded-full flex justify-center items-center") do %>
<%= icon resource.icon, color: "current" %>
<% end %>
<p><%= resource.label %></p>
</div>
@ -35,5 +35,5 @@
</div>
</div>
<%= button_to "Publish import", publish_import_path(import), class: "btn btn--primary w-full" %>
<%= render ButtonComponent.new(text: "Publish import", href: publish_import_path(import), full_width: true) %>
</div>

View file

@ -3,7 +3,7 @@
<div class="h-full flex flex-col justify-center items-center">
<div class="space-y-6 max-w-sm">
<div class="mx-auto bg-red-500/5 h-8 w-8 rounded-full flex items-center justify-center">
<%= lucide_icon "alert-octagon", class: "w-5 h-5 text-red-500" %>
<%= icon "alert-octagon", color: "destructive" %>
</div>
<div class="text-center space-y-2">
@ -11,8 +11,10 @@
<p class="text-sm text-secondary">Please try again or contact support.</p>
</div>
<div>
<%= button_to "Try again", revert_import_path(import), class: "btn btn--primary text-center w-full" %>
</div>
<%= render ButtonComponent.new(
text: "Try again",
full_width: true,
href: revert_import_path(import)
) %>
</div>
</div>

View file

@ -3,7 +3,7 @@
<div class="h-full flex flex-col justify-center items-center">
<div class="space-y-6 max-w-sm">
<div class="mx-auto bg-green-500/5 h-8 w-8 rounded-full flex items-center justify-center">
<%= lucide_icon "check", class: "w-5 h-5 text-green-500" %>
<%= icon "check", color: "success" %>
</div>
<div class="text-center space-y-2">
@ -11,8 +11,11 @@
<p class="text-sm text-secondary">Your imported data has been successfully added to the app and is now ready for use.</p>
</div>
<div>
<%= link_to "Back to dashboard", root_path, class: "block btn btn--primary text-center w-full" %>
</div>
<%= render LinkComponent.new(
text: "Back to dashboard",
variant: "primary",
full_width: true,
href: root_path
) %>
</div>
</div>

View file

@ -1,10 +1,13 @@
<div class="flex items-center justify-between">
<h1 class="text-xl font-medium text-primary"><%= t(".title") %></h1>
<%= link_to new_import_path, class: "btn btn--primary flex items-center gap-2", data: { turbo_frame: :modal } do %>
<%= lucide_icon("plus", class: "w-5 h-5") %>
<span><%= t(".new") %></span>
<% end %>
<%= render LinkComponent.new(
text: "New import",
href: new_import_path,
icon: "plus",
variant: "primary",
frame: :modal
) %>
</div>
<div class="bg-container shadow-border-xs rounded-xl p-4">

View file

@ -1,16 +1,7 @@
<%= modal do %>
<div class="p-4 space-y-4 max-w-[420px]">
<div class="space-y-2">
<div class="flex justify-between items-center">
<h2 class="font-medium text-primary"><%= t(".title") %></h2>
<button data-action="modal#close" tabindex="-1">
<%= lucide_icon("x", class: "w-5 h-5 text-primary") %>
</button>
</div>
<p class="text-secondary text-sm"><%= t(".description") %></p>
</div>
<%= render DialogComponent.new do |dialog| %>
<% dialog.with_header(title: t(".title"), subtitle: t(".description")) %>
<% dialog.with_body do %>
<div class="rounded-xl bg-container-inset p-1">
<h3 class="uppercase text-secondary text-xs font-medium px-3 py-1.5"><%= t(".sources") %></h3>
<ul class="bg-container shadow-border-xs rounded-lg">
@ -19,13 +10,15 @@
<%= link_to import_path(@pending_import), class: "flex items-center justify-between p-4 group cursor-pointer", data: { turbo: false } do %>
<div class="flex items-center gap-2">
<div class="bg-orange-500/5 rounded-md w-8 h-8 flex items-center justify-center">
<%= lucide_icon("loader", class: "w-5 h-5 text-orange-500") %>
<span class="text-orange-500">
<%= icon("loader", color: "current") %>
</span>
</div>
<span class="text-sm text-primary group-hover:text-secondary">
<%= t(".resume", type: @pending_import.type.titleize) %>
</span>
</div>
<%= lucide_icon("chevron-right", class: "w-5 h-5 text-secondary") %>
<%= icon("chevron-right") %>
<% end %>
<div class="pl-14 pr-3">
@ -39,13 +32,15 @@
<%= button_to imports_path(import: { type: "TransactionImport" }), class: "flex items-center justify-between p-4 group cursor-pointer w-full", data: { turbo: false } do %>
<div class="flex items-center gap-2">
<div class="bg-indigo-500/5 rounded-md w-8 h-8 flex items-center justify-center">
<%= lucide_icon("file-spreadsheet", class: "w-5 h-5 text-indigo-500") %>
<span class="text-indigo-500">
<%= icon("file-spreadsheet", color: "current") %>
</span>
</div>
<span class="text-sm text-primary group-hover:text-secondary">
<%= t(".import_transactions") %>
</span>
</div>
<%= lucide_icon("chevron-right", class: "w-5 h-5 text-secondary") %>
<%= icon("chevron-right") %>
<% end %>
<div class="pl-14 pr-3">
@ -59,13 +54,15 @@
<%= button_to imports_path(import: { type: "TradeImport" }), class: "flex items-center justify-between p-4 group cursor-pointer w-full", data: { turbo: false } do %>
<div class="flex items-center gap-2">
<div class="bg-yellow-500/5 rounded-md w-8 h-8 flex items-center justify-center">
<%= lucide_icon("square-percent", class: "w-5 h-5 text-yellow-500") %>
<span class="text-yellow-500">
<%= icon("square-percent", color: "current") %>
</span>
</div>
<span class="text-sm text-primary group-hover:text-secondary">
<%= t(".import_portfolio") %>
</span>
</div>
<%= lucide_icon("chevron-right", class: "w-5 h-5 text-secondary") %>
<%= icon("chevron-right") %>
<% end %>
<div class="pl-14 pr-3">
@ -79,13 +76,15 @@
<%= button_to imports_path(import: { type: "AccountImport" }), class: "flex items-center justify-between p-4 group cursor-pointer w-full", data: { turbo: false } do %>
<div class="flex items-center gap-2">
<div class="bg-violet-500/5 rounded-md w-8 h-8 flex items-center justify-center">
<%= lucide_icon("building", class: "w-5 h-5 text-violet-500") %>
<span class="text-violet-500">
<%= icon("building", color: "current") %>
</span>
</div>
<span class="text-sm text-primary group-hover:text-secondary">
<%= t(".import_accounts") %>
</span>
</div>
<%= lucide_icon("chevron-right", class: "w-5 h-5 text-secondary") %>
<%= icon("chevron-right") %>
<% end %>
<div class="pl-14 pr-3">
@ -103,7 +102,7 @@
<%= t(".import_mint") %>
</span>
</div>
<%= lucide_icon("chevron-right", class: "w-5 h-5 text-secondary") %>
<%= icon("chevron-right") %>
<% end %>
<div class="pl-14 pr-3">
@ -113,5 +112,5 @@
<% end %>
</ul>
</div>
</div>
<% end %>
<% end %>

View file

@ -1,7 +1,7 @@
<%# locals: (balance:, holdings:, cash:) %>
<div data-controller="tooltip" data-tooltip-placement-value="right" data-tooltip-offset-value=10 data-tooltip-cross-axis-value=50>
<%= lucide_icon("info", class: "w-4 h-4 shrink-0 text-secondary") %>
<%= icon("info", size: "sm") %>
<div role="tooltip" data-tooltip-target="tooltip" class="tooltip bg-gray-700 text-sm p-2 rounded w-64">
<div class="text-white">
<%= t(".total_value_tooltip") %>

View file

@ -1,3 +1,6 @@
<%= modal_form_wrapper title: t(".edit", account: @account.name) do %>
<%= render "investments/form", account: @account, url: investment_path(@account) %>
<%= render DialogComponent.new do |dialog| %>
<% dialog.with_header(title: t(".edit", account: @account.name)) %>
<% dialog.with_body do %>
<%= render "investments/form", account: @account, url: investment_path(@account) %>
<% end %>
<% end %>

View file

@ -1,7 +1,10 @@
<% if params[:step] == "method_select" %>
<%= render "accounts/new/method_selector", path: new_investment_path(return_to: params[:return_to]), us_link_token: @us_link_token, eu_link_token: @eu_link_token %>
<% else %>
<%= modal_form_wrapper title: t(".title") do %>
<%= render "investments/form", account: @account, url: investments_path %>
<%= render DialogComponent.new do |dialog| %>
<% dialog.with_header(title: t(".title")) %>
<% dialog.with_body do %>
<%= render "investments/form", account: @account, url: investments_path %>
<% end %>
<% end %>
<% end %>

View file

@ -1,20 +1,24 @@
<%= modal_form_wrapper title: t(".title"), subtitle: t(".subtitle") do %>
<%= styled_form_with model: @invitation, class: "space-y-4", data: { turbo: false } do |form| %>
<%= form.email_field :email,
required: true,
placeholder: t(".email_placeholder"),
label: t(".email_label") %>
<%= render DialogComponent.new do |dialog| %>
<% dialog.with_header(title: t(".title"), subtitle: t(".subtitle")) %>
<%= form.select :role,
options_for_select([
[t(".role_member"), "member"],
[t(".role_admin"), "admin"]
]),
{},
{ label: t(".role_label") } %>
<% dialog.with_body do %>
<%= styled_form_with model: @invitation, class: "space-y-4", data: { turbo: false } do |form| %>
<%= form.email_field :email,
required: true,
placeholder: t(".email_placeholder"),
label: t(".email_label") %>
<div class="w-full">
<%= form.submit t(".submit"), class: "bg-gray-900 text-white rounded-lg px-4 py-2 w-full" %>
</div>
<%= form.select :role,
options_for_select([
[t(".role_member"), "member"],
[t(".role_admin"), "admin"]
]),
{},
{ label: t(".role_label") } %>
<div class="w-full">
<%= form.submit t(".submit"), class: "bg-gray-900 text-white rounded-lg px-4 py-2 w-full" %>
</div>
<% end %>
<% end %>
<% end %>

View file

@ -6,10 +6,10 @@
</div>
<button data-action="clipboard#copy" class="shrink-0 z-10 inline-flex items-center px-1 text-sm text-secondary font-sm text-center" type="button">
<span data-clipboard-target="iconDefault">
<%= lucide_icon "copy", class: "w-5 h-5" %>
<%= icon "copy" %>
</span>
<span class="hidden inline-flex items-center" data-clipboard-target="iconSuccess">
<%= lucide_icon "check", class: "w-5 h-4" %>
<%= icon "check" %>
</span>
</button>
</div>

View file

@ -4,7 +4,7 @@
<%= render @invite_codes %>
<% else %>
<div class="flex flex-col items-center w-full h-64 bg-container text-center justify-center">
<%= lucide_icon "binary", class: "w-6 h-6 text-sm text-secondary" %>
<%= icon "binary", size: "lg" %>
<p class="text-base pt-4"><%= t(".no_invite_codes") %></p>
<p class="text-sm text-secondary pt-2 w-2/3"><%= t(".invite_code_description") %></p>
</div>

View file

@ -1,135 +1,140 @@
<% mobile_nav_items = [
{ name: "Home", path: root_path, icon: "pie-chart", icon_custom: false, active: page_active?(root_path) },
{ name: "Transactions", path: transactions_path, icon: "credit-card", icon_custom: false, active: page_active?(transactions_path) },
{ name: "Budgets", path: budgets_path, icon: "map", icon_custom: false, active: page_active?(budgets_path) },
{ name: "Assistant", path: chats_path, icon: "icon-assistant", icon_custom: true, active: page_active?(chats_path), mobile_only: true }
] %>
<% desktop_nav_items = mobile_nav_items.reject { |item| item[:mobile_only] } %>
<% expanded_sidebar_class = "w-full" %>
<% collapsed_sidebar_class = "w-0" %>
<%= render "layouts/shared/htmldoc" do %>
<% sidebar_config = app_sidebar_config(Current.user) %>
<div
class="flex flex-col lg:flex-row h-dvh lg:h-full bg-surface"
data-controller="app-layout"
data-app-layout-expanded-sidebar-class="<%= expanded_sidebar_class %>"
data-app-layout-collapsed-sidebar-class="<%= collapsed_sidebar_class %>"
data-app-layout-user-id-value="<%= Current.user.id %>">
<div
class="hidden fixed inset-0 bg-surface z-20 h-dvh w-full p-3 overflow-y-auto transition-all duration-300"
data-app-layout-target="mobileSidebar">
<div class="mb-2">
<%= icon("x", as_button: true, data: { action: "app-layout#closeMobileSidebar" }) %>
</div>
<div class="flex flex-col lg:flex-row h-dvh lg:h-full bg-surface pt-safe"
data-controller="sidebar"
data-sidebar-user-id-value="<%= Current.user.id %>"
data-sidebar-config-value="<%= sidebar_config.to_json %>">
<button hidden data-controller="hotkey" data-hotkey="b" data-action="sidebar#toggleLeftPanel">Toggle accounts</button>
<button hidden data-controller="hotkey" data-hotkey="l" data-action="sidebar#toggleRightPanel">Toggle chat</button>
<%= render(
"accounts/account_sidebar_tabs",
family: Current.family,
active_account_group_tab: params[:account_group_tab] || "assets"
) %>
</div>
<% unless controller_name == 'chats' %>
<nav class="flex justify-between lg:justify-start lg:flex-col shrink-0 lg:w-[84px] p-3 lg:px-0 lg:py-4 lg:mr-3">
<button data-action="sidebar#toggleLeftPanelMobile" class="lg:hidden inline-flex p-2 rounded-lg items-center justify-center hover:bg-gray-100 cursor-pointer">
<%= icon("panel-left", color: "gray") %>
</button>
<%# MOBILE - Top nav %>
<nav class="lg:hidden flex justify-between items-center p-3">
<%= icon("panel-left", as_button: true, data: { action: "app-layout#openMobileSidebar"}) %>
<%# Mobile only account sidebar groups %>
<%= tag.div class: class_names("hidden bg-surface z-20 absolute inset-0 h-dvh w-full p-4 overflow-y-auto transition-all duration-300 pt-safe"),
data: { sidebar_target: "leftPanelMobile" } do %>
<div id="account-sidebar-tabs" class="pt-6">
<div class="mb-4">
<button data-action="sidebar#toggleLeftPanelMobile">
<%= icon("x", color: "gray") %>
</button>
</div>
<%= render "accounts/account_sidebar_tabs", family: Current.family %>
</div>
<% end %>
<%= link_to root_path, class: "block" do %>
<%= image_tag "logomark-color.svg", class: "w-9 h-9 mx-auto" %>
<% end %>
<div class="lg:pl-2 lg:mb-3">
<%= render "users/user_menu", user: Current.user, placement: "bottom-end", offset: 12 %>
</nav>
<%# DESKTOP - Left navbar %>
<div class="hidden lg:block">
<nav class="h-full flex flex-col shrink-0 w-[84px] py-4 mr-3">
<div class="pl-2 mb-3">
<%= link_to root_path, class: "block" do %>
<%= image_tag "logomark-color.svg", class: "w-9 h-9 mx-auto" %>
<% end %>
</div>
<ul class="space-y-0.5 hidden lg:block">
<li>
<%= render "layouts/sidebar/nav_item", name: "Home", path: root_path, icon_key: "pie-chart" %>
</li>
<li>
<%= render "layouts/sidebar/nav_item", name: "Transactions", path: transactions_path, icon_key: "credit-card" %>
</li>
<li>
<%= render "layouts/sidebar/nav_item", name: "Budgets", path: budgets_path, icon_key: "map" %>
</li>
<ul class="space-y-0.5">
<% desktop_nav_items.reject { |item| item[:mobile_only] }.each do |nav_item| %>
<li>
<%= render "layouts/shared/nav_item", **nav_item %>
</li>
<% end %>
</ul>
<div class="lg:pl-2 lg:mt-auto lg:mx-auto">
<div class="lg:hidden">
<%= render "users/user_menu", user: Current.user, placement: "bottom-end", offset: 12 %>
</div>
<div class="pl-2 mt-auto mx-auto flex flex-col gap-2">
<%= render ButtonComponent.new(
variant: "icon",
icon: "message-circle-question",
data: { action: "intercom#show" }
) %>
<div class="hidden lg:block">
<%= render "users/user_menu", user: Current.user %>
</div>
<%= render "users/user_menu", user: Current.user %>
</div>
</nav>
</div>
<%# DESKTOP - Left sidebar %>
<%= tag.div class: class_names(
"hidden lg:block py-4 overflow-y-auto shrink-0 max-w-[320px] transition-all duration-300",
Current.user.show_sidebar? ? expanded_sidebar_class : collapsed_sidebar_class,
),
data: { app_layout_target: "leftSidebar" } do %>
<% if content_for?(:sidebar) %>
<%= yield :sidebar %>
<% else %>
<div id="account-sidebar-tabs" data-turbo-permanent>
<%= render "accounts/account_sidebar_tabs", family: Current.family, active_account_group_tab: params[:account_group_tab] || "assets" %>
</div>
<% end %>
<% end %>
<div class="flex justify-between lg:justify-normal grow overflow-y-auto">
<%= tag.div class: class_names("py-4 shrink-0 h-full overflow-y-auto transition-all duration-300 hidden lg:block"),
style: "width: #{sidebar_config.dig(:left_panel, :initial_width)}px",
data: { sidebar_target: "leftPanel" } do %>
<% if content_for?(:sidebar) %>
<%= yield :sidebar %>
<% else %>
<div id="account-sidebar-tabs" data-turbo-permanent>
<%= render "accounts/account_sidebar_tabs", family: Current.family %>
</div>
<% end %>
<% end %>
<%# SHARED - Main content %>
<%= tag.main class: class_names("grow overflow-y-auto px-3 lg:px-10 py-4 h-full w-full mx-auto max-w-5xl"), data: { app_layout_target: "content" } do %>
<div class="hidden lg:flex gap-2 items-center justify-between mb-6">
<div class="flex items-center gap-2">
<%= icon("panel-left", as_button: true, data: { action: "app-layout#toggleLeftSidebar" }) %>
<%= tag.main class: class_names("px-3 lg:px-10 py-4 grow h-full", require_upgrade? ? "relative overflow-hidden" : "overflow-y-auto") do %>
<% if require_upgrade? %>
<div class="absolute inset-0 px-3 lg:px-10 h-full w-full z-50">
<%= render "shared/subscribe_modal" %>
</div>
<% end %>
<%= tag.div class: class_names("mx-auto max-w-5xl w-full h-full"), data: { sidebar_target: "content" } do %>
<% if content_for?(:breadcrumbs) %>
<%= yield :breadcrumbs %>
<% else %>
<%= render "layouts/shared/breadcrumbs", breadcrumbs: @breadcrumbs %>
<% end %>
</div>
<%= icon("panel-right", as_button: true, data: { action: "app-layout#toggleRightSidebar" }) %>
</div>
<% if content_for?(:page_header) %>
<%= yield :page_header %>
<% end %>
<%= yield %>
<% end %>
<% if content_for?(:page_header) %>
<%= yield :page_header %>
<% end %>
<%# AI chat sidebar %>
<%= tag.div id: "chat-container",
style: "width: #{sidebar_config.dig(:right_panel, :initial_width)}px; overflow: #{sidebar_config.dig(:right_panel, :overflow)}",
class: class_names("flex flex-col justify-between shrink-0 transition-all duration-300 hidden lg:block"),
data: { controller: "chat hotkey", sidebar_target: "rightPanel", turbo_permanent: true } do %>
<%= yield %>
<% end %>
<% if Current.user.ai_enabled? %>
<%# DESKTOP - Right sidebar %>
<%= tag.div class: class_names(
"hidden lg:block h-full overflow-y-auto shrink-0 max-w-[400px] transition-all duration-300",
Current.user.show_ai_sidebar? ? expanded_sidebar_class : collapsed_sidebar_class,
),
data: { app_layout_target: "rightSidebar" } do %>
<%= tag.div id: "chat-container", class: "relative h-full", data: { controller: "chat hotkey", turbo_permanent: true } do %>
<div class="flex flex-col h-full justify-between shrink-0">
<%= turbo_frame_tag chat_frame, src: chat_view_path(@chat), loading: "lazy", class: "h-full" do %>
<div class="flex justify-center items-center h-full">
<%= lucide_icon("loader-circle", class: "w-5 h-5 text-secondary animate-spin") %>
<%= icon("loader-circle", class: "animate-spin") %>
</div>
<% end %>
<% else %>
<%= render "chats/ai_consent" %>
</div>
<% unless Current.user.ai_enabled? %>
<div class="absolute backdrop-blur-lg inset-0 h-full w-full flex flex-col justify-center items-center pl-0.5 pr-4">
<%= render "chats/ai_consent" %>
</div>
<% end %>
<% end %>
</div>
<% end %>
<nav class="lg:hidden bg-surface md:bg-container shrink-0 z-10 pb-2 border-t border-tertiary pb-safe">
<ul class="flex items-center justify-around gap-1">
<li>
<%= render "layouts/sidebar/nav_item", name: "Home", path: root_path, icon_key: "pie-chart" %>
</li>
<li>
<%= render "layouts/sidebar/nav_item", name: "Transactions", path: transactions_path, icon_key: "credit-card" %>
</li>
<li>
<%= render "layouts/sidebar/nav_item", name: "Budgets", path: budgets_path, icon_key: "map" %>
</li>
<li>
<%= render "layouts/sidebar/nav_item", name: "Assistant", path: chats_path, icon_key: "icon-assistant", is_custom: true %>
</li>
</ul>
</nav>
<%# MOBILE - Bottom Nav %>
<%= tag.nav class: "lg:hidden bg-surface shrink-0 z-10 pb-2 border-t border-tertiary pb-safe flex justify-around" do %>
<% mobile_nav_items.each do |nav_item| %>
<%= render "layouts/shared/nav_item", **nav_item %>
<% end %>
<% end %>
</div>
<% end %>

View file

@ -1,17 +1,21 @@
<%= render "layouts/shared/htmldoc" do %>
<div class="flex flex-col h-dvh bg-container pt-safe">
<header class="flex items-center justify-between p-8">
<%= link_to content_for(:previous_path) || imports_path do %>
<%= lucide_icon "arrow-left", class: "w-5 h-5 text-secondary" %>
<% end %>
<%= render LinkComponent.new(
variant: "icon",
icon: "arrow-left",
href: content_for(:previous_path) || imports_path
) %>
<nav>
<%= yield :header_nav %>
</nav>
<%= link_to imports_path do %>
<%= lucide_icon "x", class: "text-secondary w-5 h-5" %>
<% end %>
<%= render LinkComponent.new(
variant: "icon",
icon: "x",
href: imports_path
) %>
</header>
<main class="grow px-8 md:pt-12 pb-32 overflow-y-auto">

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html data-theme="<%= params.dig(:lookbook, :display, :theme) %>">
<head>
<title>Component Preview</title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %>
<%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %>
<%= javascript_importmap_tags %>
</head>
<body class="p-4 h-full bg-container <%= params.dig(:lookbook, :display, :container_classes) %>">
<%= yield %>
</body>
</html>

View file

@ -10,7 +10,7 @@
<% if content_for?(:breadcrumbs) %>
<%= yield :breadcrumbs %>
<% else %>
<%= render "layouts/shared/breadcrumbs", breadcrumbs: @breadcrumbs, sidebar_toggle_enabled: false %>
<%= render "layouts/shared/breadcrumbs", breadcrumbs: @breadcrumbs %>
<% end %>
<% if content_for?(:page_title) %>

View file

@ -1,33 +1,17 @@
<%# locals: (breadcrumbs:, sidebar_toggle_enabled: true) %>
<%# locals: (breadcrumbs:) %>
<nav class="items-center gap-2 mb-6 hidden md:flex">
<% if sidebar_toggle_enabled %>
<button data-action="sidebar#toggleLeftPanel" class="hidden p-2 lg:inline-flex rounded-lg items-center justify-center hover:bg-container-inset cursor-pointer">
<%= icon("panel-left", color: "gray") %>
</button>
<% end %>
<div class="py-2 flex items-center gap-2">
<% breadcrumbs.each_with_index do |(name, path), index| %>
<% if index > 0 %>
<%= icon("chevron-right", color: "gray", size: "sm") %>
<% end %>
<% if path.present? && index < breadcrumbs.size - 1 %>
<%= link_to name, path, class: "text-sm text-gray-500 font-medium" %>
<% elsif index == breadcrumbs.size - 1 %>
<span class="text-primary font-medium text-sm"><%= name %></span>
<% else %>
<span class="text-sm text-gray-500 font-medium"><%= name %></span>
<% end %>
<div class="py-2 flex items-center gap-2">
<% breadcrumbs.each_with_index do |(name, path), index| %>
<% if index > 0 %>
<%= icon("chevron-right", color: "gray", size: "sm") %>
<% end %>
</div>
<% if sidebar_toggle_enabled %>
<div class="ml-auto">
<button data-action="sidebar#toggleRightPanel" class="p-2 hidden lg:inline-flex rounded-lg items-center justify-center hover:bg-container-inset cursor-pointer" title="Toggle AI Assistant">
<%= icon("panel-right", color: "gray") %>
</button>
</div>
<% if path.present? && index < breadcrumbs.size - 1 %>
<%= link_to name, path, class: "text-sm text-gray-500 font-medium" %>
<% elsif index == breadcrumbs.size - 1 %>
<span class="text-primary font-medium text-sm"><%= name %></span>
<% else %>
<span class="text-sm text-gray-500 font-medium"><%= name %></span>
<% end %>
<% end %>
</nav>
</div>

View file

@ -0,0 +1,30 @@
<%# This dialog is used as an override to the browser's confirm API when submitting forms with data-turbo-confirm %>
<%# See confirm_dialog_controller.js and _htmldoc.html.erb %>
<%= render DialogComponent.new(id: "confirm-dialog", auto_open: false, data: { controller: "confirm-dialog" }, width: "sm") do |dialog| %>
<% dialog.with_body do %>
<form method="dialog" class="space-y-4">
<div class="space-y-2">
<div class="flex items-center justify-between gap-2">
<h3 class="font-medium text-primary" data-confirm-dialog-target="title">Are you sure?</h3>
<%= icon("x", as_button: true, type: "submit", value: "cancel") %>
</div>
<p class="text-sm text-secondary" data-confirm-dialog-target="subtitle">This action cannot be undone.</p>
</div>
<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,
) %>
<% end %>
</div>
</form>
<% end %>
<% end %>

Some files were not shown because too many files have changed in this diff Show more