diff --git a/Gemfile b/Gemfile
index 42d78dd4..7a2b13c0 100644
--- a/Gemfile
+++ b/Gemfile
@@ -17,6 +17,7 @@ gem "bootsnap", require: false
gem "importmap-rails"
gem "propshaft"
gem "tailwindcss-rails"
+gem "lucide-rails", github: "maybe-finance/lucide-rails"
# Hotwire
gem "stimulus-rails"
diff --git a/Gemfile.lock b/Gemfile.lock
index 48fb1ef6..3f0464be 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,3 +1,10 @@
+GIT
+ remote: https://github.com/maybe-finance/lucide-rails.git
+ revision: 6170b3a0eceb43a8af6552638e9526673c356d0d
+ specs:
+ lucide-rails (0.2.0)
+ railties (>= 4.1.0)
+
GIT
remote: https://github.com/rails/rails.git
revision: bab4aa7cb25112846e04cea907361a1de0f126ef
@@ -325,7 +332,7 @@ GEM
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
smart_properties (1.17.0)
- sorbet-runtime (0.5.11226)
+ sorbet-runtime (0.5.11234)
stimulus-rails (1.3.3)
railties (>= 6.0.0)
stringio (3.1.0)
@@ -388,6 +395,7 @@ DEPENDENCIES
inline_svg
jbuilder
letter_opener
+ lucide-rails!
money-rails (~> 1.12)
pg (~> 1.1)
propshaft
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index bd65adc7..1904451f 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -10,4 +10,10 @@ module ApplicationHelper
def permitted_accountable_partial(name)
name.underscore
end
+
+ # Wrap view with <%= modal do %> ... <% end %> to have it open in a modal
+ # Make sure to add data-turbo-frame="modal" to the link/button that opens the modal
+ def modal(&block)
+ render "shared/modal", &block
+ end
end
diff --git a/app/javascript/controllers/hotkey_controller.js b/app/javascript/controllers/hotkey_controller.js
new file mode 100644
index 00000000..88df7f9d
--- /dev/null
+++ b/app/javascript/controllers/hotkey_controller.js
@@ -0,0 +1,13 @@
+import { Controller } from "@hotwired/stimulus"
+import { install, uninstall } from "@github/hotkey"
+
+// Connects to data-controller="hotkey"
+export default class extends Controller {
+ connect() {
+ install(this.element)
+ }
+
+ disconnect() {
+ uninstall(this.element)
+ }
+}
diff --git a/app/javascript/controllers/list_keyboard_navigation_controller.js b/app/javascript/controllers/list_keyboard_navigation_controller.js
new file mode 100644
index 00000000..9a019e86
--- /dev/null
+++ b/app/javascript/controllers/list_keyboard_navigation_controller.js
@@ -0,0 +1,36 @@
+import { Controller } from "@hotwired/stimulus"
+
+// Connects to data-controller="list-keyboard-navigation"
+export default class extends Controller {
+ focusPrevious() {
+ this.focusLinkTargetInDirection(-1)
+ }
+
+ focusNext() {
+ this.focusLinkTargetInDirection(1)
+ }
+
+ focusLinkTargetInDirection(direction) {
+ const element = this.getLinkTargetInDirection(direction)
+ element?.focus()
+ }
+
+ getLinkTargetInDirection(direction) {
+ const indexOfLastFocus = this.indexOfLastFocus()
+ return this.focusableLinks[indexOfLastFocus + direction]
+ }
+
+ indexOfLastFocus(targets = this.focusableLinks) {
+ const indexOfActiveElement = targets.indexOf(document.activeElement)
+
+ if (indexOfActiveElement !== -1) {
+ return indexOfActiveElement
+ } else {
+ return targets.findIndex(target => target.getAttribute("tabindex") === "0")
+ }
+ }
+
+ get focusableLinks() {
+ return Array.from(this.element.querySelectorAll("a[href]"))
+ }
+}
diff --git a/app/javascript/controllers/modal_controller.js b/app/javascript/controllers/modal_controller.js
new file mode 100644
index 00000000..e6df3a19
--- /dev/null
+++ b/app/javascript/controllers/modal_controller.js
@@ -0,0 +1,15 @@
+import { Controller } from "@hotwired/stimulus"
+
+// Connects to data-controller="modal"
+export default class extends Controller {
+ connect() {
+ this.element.showModal();
+ }
+
+ // Hide the dialog when the user clicks outside of it
+ click_outside(e) {
+ if (e.target === this.element) {
+ this.element.close();
+ }
+ }
+}
diff --git a/app/views/accounts/_account_type.html.erb b/app/views/accounts/_account_type.html.erb
index 746e1f72..d09ca7cf 100644
--- a/app/views/accounts/_account_type.html.erb
+++ b/app/views/accounts/_account_type.html.erb
@@ -1,9 +1,6 @@
-
- <%= link_to new_account_path(type: type.class.name.demodulize), class: "flex flex-col items-center justify-center w-full text-center focus:outline-none" do %>
-
-
- <%= inline_svg_tag(icon, class: "#{text_color} stroke-current") %>
-
- <%= type.model_name.human %>
- <% end %>
-
+<%= link_to new_account_path(step: 'method', type: type.class.name.demodulize), class: "flex items-center gap-4 w-full text-center focus:outline-none focus:bg-gray-50 border border-transparent focus:border focus:border-gray-200 block px-2 hover:bg-gray-50 rounded-lg p-2" do %>
+
+ <%= lucide_icon(icon, class: "#{text_color} w-5 h-5") %>
+
+ <%= type.model_name.human %>
+<% end %>
diff --git a/app/views/accounts/_entry_method.html.erb b/app/views/accounts/_entry_method.html.erb
new file mode 100644
index 00000000..31277d4f
--- /dev/null
+++ b/app/views/accounts/_entry_method.html.erb
@@ -0,0 +1,15 @@
+<% if local_assigns[:disabled] && disabled %>
+
+
+ <%= lucide_icon(icon, class: "text-gray-500 w-5 h-5") %>
+
+ <%= text %>
+
+<% else %>
+ <%= link_to new_account_path(type: type.class.name.demodulize), class: "flex items-center gap-4 w-full text-center focus:outline-none focus:bg-gray-50 border border-transparent focus:border focus:border-gray-200 px-2 hover:bg-gray-50 rounded-lg p-2" do %>
+
+ <%= lucide_icon(icon, class: "text-gray-500 w-5 h-5") %>
+
+ <%= text %>
+ <% end %>
+<% end %>
\ No newline at end of file
diff --git a/app/views/accounts/account/_depository.html.erb b/app/views/accounts/account/_depository.html.erb
index 11933f8d..a6f1670c 100644
--- a/app/views/accounts/account/_depository.html.erb
+++ b/app/views/accounts/account/_depository.html.erb
@@ -1,4 +1,4 @@
-
-
- <%= f.select :subtype, options_for_select([["Checking", "checking"], ["Savings", "savings"]], selected: ""), {}, class: "block w-full p-0 mt-1 bg-transparent border-none focus:outline-none focus:ring-0" %>
-
+
+
+ <%= f.select :subtype, options_for_select([["Checking", "checking"], ["Savings", "savings"]], selected: ""), {}, class: "block w-full p-0 mt-1 bg-transparent border-none focus:outline-none focus:ring-0" %>
+
diff --git a/app/views/accounts/account/_investment.html.erb b/app/views/accounts/account/_investment.html.erb
index 6310d930..41b46a78 100644
--- a/app/views/accounts/account/_investment.html.erb
+++ b/app/views/accounts/account/_investment.html.erb
@@ -1,4 +1,4 @@
-
+
<%= f.select :subtype, options_for_select(Account::Investment::SUBTYPES, selected: ""), {}, class: "block w-full p-0 mt-1 bg-transparent border-none focus:outline-none focus:ring-0" %>
diff --git a/app/views/accounts/new.html.erb b/app/views/accounts/new.html.erb
index 58ba7ff9..23d8e8b7 100644
--- a/app/views/accounts/new.html.erb
+++ b/app/views/accounts/new.html.erb
@@ -1,47 +1,94 @@
<%= t('.title')%>
-<% if params[:type].blank? || Account.accountable_types.include?("Account::#{params[:type]}") == false %>
-
- <%= render "account_type", type: Account::Credit.new, bg_color: "bg-[#E6F6FA]", text_color: "text-[#189FC7]", icon: "icon-credit-card.svg" %>
- <%= render "account_type", type: Account::Depository.new, bg_color: "bg-[#EAF4FF]", text_color: "text-[#3492FB]", icon: "icon-bank-accounts.svg" %>
- <%= render "account_type", type: Account::Investment.new, bg_color: "bg-[#EDF7F4]", text_color: "text-[#1BD5A1]", icon: "icon-bank-accounts.svg" %>
- <%= render "account_type", type: Account::Loan.new, bg_color: "bg-[#EDF7F4]", text_color: "text-[#1BD5A1]", icon: "icon-bank-accounts.svg" %>
- <%= render "account_type", type: Account::OtherAsset.new, bg_color: "bg-[#EDF7F4]", text_color: "text-[#1BD5A1]", icon: "icon-bank-accounts.svg" %>
- <%= render "account_type", type: Account::OtherLiability.new, bg_color: "bg-[#EDF7F4]", text_color: "text-[#1BD5A1]", icon: "icon-bank-accounts.svg" %>
- <%= render "account_type", type: Account::Property.new, bg_color: "bg-[#FEF0F7]", text_color: "text-[#F03695]", icon: "icon-real-estate.svg" %>
- <%= render "account_type", type: Account::Vehicle.new, bg_color: "bg-[#EDF7F4]", text_color: "text-[#1BD5A1]", icon: "icon-bank-accounts.svg" %>
-
-<% else %>
-
- <%= link_to new_account_path, class: "" do %>
- <%= inline_svg_tag('icon-arrow-left.svg', class: 'text-gray-500 fill-current') %>
- <% end %>
-
<%= t('.enter_type_account', type: @account.accountable_class.model_name.human.downcase ) %>
-
-
- <%= form_with model: @account, url: accounts_path, scope: :account, html: { class: "space-y-4" } do |f| %>
- <%= f.hidden_field :accountable_type %>
-
-
- <%#
Optional %>
- <%= f.label :name, class: 'block text-sm font-medium opacity-50 focus-within:opacity-100' %>
- <%= f.text_field :name, placeholder: t('.account_name_placeholder'), required: 'required', class: "p-0 mt-1 bg-transparent border-none opacity-50 focus:outline-none focus:ring-0 focus-within:opacity-100" %>
+<%= modal do %>
+ <% if params[:type].blank? || Account.accountable_types.include?("Account::#{params[:type]}") == false %>
+
+ What would you like to add?
+
+
+
- <%= render "accounts/#{permitted_accountable_partial(@account.accountable_type)}", f: f %>
-
-
-
- <%= f.label :balance, class: 'block text-sm font-medium opacity-50 focus-within:opacity-100' %>
-
- <%= f.number_field :balance, placeholder: number_to_currency(0), in: 0.00..100000000.00, required: 'required', class: "p-0 mt-1 bg-transparent border-none opacity-50 focus:outline-none focus:ring-0 focus-within:opacity-100" %>
+ <%= render "account_type", type: Account::Depository.new, bg_color: "bg-[#EFF8FF]", text_color: "text-[#2E90FA]", icon: "landmark" %>
+ <%= render "account_type", type: Account::Investment.new, bg_color: "bg-[#ECFDF3]", text_color: "text-[#32D583]", icon: "line-chart" %>
+ <%= render "account_type", type: Account::Property.new, bg_color: "bg-[#FCF5F9]", text_color: "text-[#F23E94]", icon: "home" %>
+ <%= render "account_type", type: Account::Vehicle.new, bg_color: "bg-[#EEF4FF]", text_color: "text-[#6172F3]", icon: "car-front" %>
+ <%= render "account_type", type: Account::Credit.new, bg_color: "bg-[#F0F9FF]", text_color: "text-[#36BFFA]", icon: "credit-card" %>
+ <%= render "account_type", type: Account::Loan.new, bg_color: "bg-[#FEF6EE]", text_color: "text-[#F38744]", icon: "hand-coins" %>
+ <%= render "account_type", type: Account::OtherAsset.new, bg_color: "bg-[#ECFDF3]", text_color: "text-[#12B76A]", icon: "plus" %>
+ <%= render "account_type", type: Account::OtherLiability.new, bg_color: "bg-[#FEF3F2]", text_color: "text-[#F04438]", icon: "minus" %>
+
+
+
+
+ Select <%= lucide_icon('corner-down-left', class: 'inline w-3 h-3')%>
+
+
+ Navigate <%= lucide_icon('arrow-up', class: 'inline w-3 h-3')%> <%= lucide_icon('arrow-down', class: 'inline w-3 h-3')%>
+
+
+
+ Close ESC
-
-
-
+ <% elsif params[:step] == 'method' && params[:type].present? %>
+
+ <%= link_to new_account_path, class: "flex w-8 h-8 shrink-0 grow-0 items-center justify-center rounded-lg bg-[#141414]/5" do %>
+ <%= lucide_icon('arrow-left', class: 'text-gray-500 w-5 h-5') %>
+ <% end %>
+ How would you like to add it?
+
+
+
+
+ <%= render "entry_method", type: Account::Depository.new, text: 'Enter account balance manually', icon: "keyboard" %>
+ <%= render "entry_method", type: Account::Depository.new, text: 'Securely link bank account with data provider (coming soon)', icon: "link-2", disabled: true %>
+ <%= render "entry_method", type: Account::Depository.new, text: 'Upload spreadsheet (coming soon)', icon: "sheet", disabled: true %>
+
+
+
+
+ Select <%= lucide_icon('corner-down-left', class: 'inline w-3 h-3')%>
+
+
+ Navigate <%= lucide_icon('arrow-up', class: 'inline w-3 h-3')%> <%= lucide_icon('arrow-down', class: 'inline w-3 h-3')%>
+
+
+
+ Close ESC
+
+
+ <% else %>
+
+ <%= link_to new_account_path(step: 'method', type: params[:type]), class: "flex w-8 h-8 shrink-0 grow-0 items-center justify-center rounded-lg bg-[#141414]/5" do %>
+ <%= lucide_icon('arrow-left', class: 'text-gray-500 w-5 h-5') %>
+ <% end %>
+ Add account
+
+
+ <%= form_with model: @account, url: accounts_path, scope: :account, html: { class: "space-y-4 m-5 mt-1", data: { turbo: false } } do |f| %>
+ <%= f.hidden_field :accountable_type %>
+
+
+ <%= f.label :name, 'Account name', class: 'block text-sm font-medium opacity-50 focus-within:opacity-100' %>
+ <%= f.text_field :name, placeholder: 'Example account name', required: 'required', class: "p-0 mt-1 bg-transparent border-none opacity-50 focus:outline-none focus:ring-0 focus-within:opacity-100" %>
+
+
+ <%= render "accounts/#{permitted_accountable_partial(@account.accountable_type)}", f: f %>
+
+
+ <%= f.label :balance, class: 'block text-sm font-medium opacity-50 focus-within:opacity-100' %>
+
+ <%= f.number_field :balance, placeholder: number_to_currency(0), in: 0.00..100000000.00, required: 'required', class: "p-0 mt-1 bg-transparent border-none opacity-50 focus:outline-none focus:ring-0 focus-within:opacity-100 w-full" %>
+
+
+
+
+
+
+ <% end %>
<% end %>
<% end %>
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
index fd46221a..553132a6 100644
--- a/app/views/layouts/application.html.erb
+++ b/app/views/layouts/application.html.erb
@@ -59,10 +59,11 @@
<%= link_to accounts_path, class: 'text-xs' do%>
<%= t('.accounts') %>
<% end %>
- <%= link_to new_account_path, class: 'block hover:bg-gray-100 p-2 text-sm font-semibold text-gray-900 flex items-center rounded', title: t('.new_account') do %>
+ <%= link_to new_account_path, class: 'block hover:bg-gray-100 p-2 text-sm font-semibold text-gray-900 flex items-center rounded', title: t('.new_account'), data: { turbo_frame: "modal" } do %>
<%= inline_svg_tag('icon-add.svg', class: 'text-gray-500 fill-current') %>
<% end %>
+
<%= t('.cash') %>
@@ -83,5 +84,6 @@
<%= yield %>
+ <%= turbo_frame_tag "modal" %>