1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-09 15:35:22 +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

@ -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 %>

View file

@ -1,6 +0,0 @@
<%= turbo_frame_tag "modal" %>
<%= turbo_frame_tag "drawer" %>
<%= render "shared/confirm_modal" %>
<%= render "impersonation_sessions/super_admin_bar" if Current.true_user&.super_admin? && show_super_admin_bar? %>
<%= render "impersonation_sessions/approval_bar" if Current.true_user&.impersonated_support_sessions&.initiated&.any? %>

View file

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html class="h-full text-primary overflow-hidden lg:overflow-auto font-sans <%= @os %>" lang="en" data-controller="theme" data-theme-user-preference-value="<%= Current.user&.theme || "system" %>">
<html class="h-full text-primary overflow-hidden lg:overflow-auto font-sans <%= @os %>" lang="en" data-controller="theme intercom" data-theme-user-preference-value="<%= Current.user&.theme || "system" %>">
<head>
<%= render "layouts/shared/head" %>
<%= yield :head %>
@ -20,9 +20,22 @@
<%= family_stream %>
<% if Rails.env.development? %>
<div class="fixed bottom-32 left-7 flex flex-col gap-1">
<%= icon("eclipse", as_button: true, data: { action: "theme#toDark" }) %>
<%= icon("sun", as_button: true, data: { action: "theme#toLight" }) %>
</div>
<% end %>
<% if require_upgrade? %>
<%= render "shared/subscribe_modal" %>
<% end %>
<%= turbo_frame_tag "modal" %>
<%= turbo_frame_tag "drawer" %>
<%= render "shared/confirm_modal" %>
<%# Custom overrides for browser's confirm API %>
<%= render "layouts/shared/confirm_dialog" %>
<%= render "impersonation_sessions/super_admin_bar" if Current.true_user&.super_admin? && show_super_admin_bar? %>
<%= render "impersonation_sessions/approval_bar" if Current.true_user&.impersonated_support_sessions&.initiated&.any? %>

View file

@ -0,0 +1,20 @@
<%# locals:(name:, path:, icon:, icon_custom:, active:, mobile_only: false) %>
<%= link_to path, class: "space-y-1 group block relative pb-1" do %>
<div class="grow flex flex-col lg:flex-row gap-1 items-center">
<%= tag.div class: class_names("w-4 h-1 lg:w-1 lg:h-4 rounded-bl-sm rounded-br-sm lg:rounded-tr-sm lg:rounded-br-sm lg:rounded-bl-none", "bg-nav-indicator" => active) %>
<%= tag.div class: class_names(
"w-8 h-8 flex items-center justify-center mx-auto rounded-lg",
active ? "bg-container shadow-xs text-primary" : "group-hover:bg-container-hover text-secondary"
) do %>
<%= icon(icon, color: active ? "current" : "default", custom: icon_custom) %>
<% end %>
</div>
<div class="grow flex justify-center lg:pl-2">
<%= tag.p class: class_names("font-medium text-[11px]", active ? "text-primary" : "text-secondary") do %>
<%= name %>
<% end %>
</div>
<% end %>

View file

@ -1,18 +0,0 @@
<%# locals: (name:, path:, icon_key:, is_custom: false) %>
<%= link_to path, class: "space-y-1 group block relative pb-1" do %>
<div class="grow flex flex-col lg:flex-row gap-1 items-center">
<%= tag.div class: class_names("w-4 h-1 lg:w-1 lg:h-4 rounded-bl-sm rounded-br-sm lg:rounded-tr-sm lg:rounded-br-sm lg:rounded-bl-none", "bg-nav-indicator" => page_active?(path)) %>
<% icon_color = page_active?(path) ? "current" : "gray" %>
<%= tag.div class: class_names("w-8 h-8 flex items-center justify-center mx-auto rounded-lg", page_active?(path) ? "bg-container shadow-xs text-primary" : "group-hover:bg-container-hover text-secondary") do %>
<%= is_custom ? icon_custom(icon_key, color: icon_color) : icon(icon_key, color: icon_color) %>
<% end %>
</div>
<div class="grow flex justify-center lg:pl-2">
<%= tag.p class: class_names("font-medium text-[11px]", page_active?(path) ? "text-primary" : "text-secondary") do %>
<%= name %>
<% end %>
</div>
<% end %>

View file

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