1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-09 23:45:21 +02:00

Dialog replacements

This commit is contained in:
Zach Gollwitzer 2025-04-29 15:14:29 -04:00
parent 779ca08d95
commit 9a67e42176
27 changed files with 344 additions and 253 deletions

View file

@ -259,6 +259,12 @@
@apply text-gray-200;
}
/* We control the sizing through DialogComponent, so reset this value */
dialog:modal {
max-width: 100dvw;
max-height: 100dvh;
}
details>summary::-webkit-details-marker {
@apply hidden;
}

View file

@ -0,0 +1,38 @@
<turbo-frame id="<%= frame %>">
<%= tag.dialog class: "w-full h-full bg-transparent backdrop:bg-overlay #{drawer? ? "lg:p-3" : "lg:p-1"}", **merged_opts do %>
<%= tag.div class: dialog_outer_classes do %>
<%= tag.div class: dialog_inner_classes do %>
<div class="grow overflow-y-auto py-4 space-y-4">
<% if header? %>
<%= header %>
<% end %>
<% if body? %>
<div class="px-4">
<%= body %>
<% if sections.any? %>
<div class="space-y-4">
<% sections.each do |section| %>
<%= section %>
<% end %>
</div>
<% end %>
</div>
<% end %>
<%# Optional, for customizing dialogs %>
<%= content %>
</div>
<% if actions? %>
<div class="flex items-center gap-2 justify-end p-4">
<% actions.each do |action| %>
<%= action %>
<% end %>
</div>
<% end %>
<% end %>
<% end %>
<% end %>
</turbo-frame>

View file

@ -0,0 +1,93 @@
class DialogComponent < ViewComponent::Base
renders_one :header, ->(title: nil, subtitle: nil, hide_close_icon: false, **opts, &block) do
content_tag(:header, class: "px-4 flex flex-col gap-2", **opts) do
title_div = content_tag(:div, class: "flex items-center justify-between gap-2") do
title = content_tag(:h2, title, class: class_names("font-medium text-primary", drawer? ? "text-lg" : "")) if title
close_icon = render ButtonComponent.new(variant: "icon", icon: "x", tabindex: "-1", data: { action: "dialog#close" }) unless hide_close_icon
safe_join([title, close_icon].compact)
end
subtitle = content_tag(:p, subtitle, class: "text-sm text-secondary") if subtitle
block_content = capture(&block) if block
safe_join([title_div, subtitle, block_content].compact)
end
end
renders_one :body
renders_many :actions, ->(cancel_action: false, **button_opts) do
merged_opts = if cancel_action
button_opts.merge(type: "button", data: { action: "modal#close" })
else
button_opts
end
render ButtonComponent.new(**merged_opts)
end
renders_many :sections, ->(title:, **disclosure_opts, &block) do
render DisclosureComponent.new(title: title, align: :right, **disclosure_opts) do
block.call
end
end
attr_reader :variant, :open_on_load, :reload_on_close, :opts
VARIANTS = %w[modal drawer].freeze
def initialize(variant: "modal", open_on_load: false, reload_on_close: false, frame: nil, **opts)
@variant = variant.to_sym
@open_on_load = open_on_load
@reload_on_close = reload_on_close
@frame = frame
@opts = opts
end
def frame
@frame || variant
end
def dialog_outer_classes
variant_classes = if drawer?
"items-end justify-end"
else
"items-center justify-center"
end
class_names(
"flex h-full w-full",
variant_classes
)
end
def dialog_inner_classes
variant_classes = if drawer?
"lg:w-[480px] h-full"
else
"max-w-lg max-h-full"
end
class_names(
"flex flex-col bg-container lg:rounded-xl lg:shadow-border-xs w-full overflow-hidden",
variant_classes
)
end
def merged_opts
merged_opts = opts.dup
data = merged_opts.delete(:data) || {}
data[:controller] = ["dialog", data[:controller]].compact.join(" ")
data[:dialog_open_on_load_value] = open_on_load
data[:dialog_reload_on_close_value] = reload_on_close
merged_opts[:data] = data
merged_opts
end
def drawer?
variant == :drawer
end
end

View file

@ -1,14 +1,17 @@
import { Controller } from "@hotwired/stimulus";
// Connects to data-controller="modal"
// Connects to data-controller="dialog"
export default class extends Controller {
static values = {
openOnLoad: { type: Boolean, default: true },
reloadOnClose: { type: Boolean, default: false },
};
connect() {
if (this.element.open) return;
this.element.showModal();
if (this.openOnLoadValue) {
this.element.showModal();
}
}
// Hide the dialog when the user clicks outside of it

View file

@ -22,4 +22,4 @@
<div class="mt-2">
<%= content %>
</div>
</details>
</details>

View file

@ -44,32 +44,6 @@ module ApplicationHelper
turbo_stream_from Current.family if Current.family
end
##
# Helper to open a centered and overlayed modal with custom contents
#
# @example Basic usage
# <%= modal classes: "custom-class" do %>
# <div>Content here</div>
# <% end %>
#
def modal(reload_on_close: false, overflow_visible: false, &block)
content = capture &block
render partial: "shared/modal", locals: { content:, reload_on_close:, overflow_visible: }
end
##
# Helper to open a drawer on the right side of the screen with custom contents
#
# @example Basic usage
# <%= drawer do %>
# <div>Content here</div>
# <% end %>
#
def drawer(reload_on_close: false, &block)
content = capture &block
render partial: "shared/drawer", locals: { content:, reload_on_close: }
end
def page_active?(path)
current_page?(path) || (request.path.start_with?(path) && path != "/")
end

View file

@ -81,7 +81,7 @@ class StyledFormBuilder < ActionView::Helpers::FormBuilder
@template.render(
ButtonComponent.new(
text: value,
data: { turbo_submits_with: "Submitting..." },
data: (options[:data] || {}).merge({ turbo_submits_with: "Submitting..." }),
full_width: true
)
)

View file

@ -7,7 +7,7 @@ export default class extends Controller {
"group",
"selectionBar",
"selectionBarText",
"bulkEditDrawerTitle",
"bulkEditDrawerHeader",
];
static values = {
singularLabel: String,
@ -25,8 +25,9 @@ export default class extends Controller {
document.removeEventListener("turbo:load", this._updateView);
}
bulkEditDrawerTitleTargetConnected(element) {
element.innerText = `Edit ${
bulkEditDrawerHeaderTargetConnected(element) {
const headingTextEl = element.querySelector("h2");
headingTextEl.innerText = `Edit ${
this.selectedIdsValue.length
} ${this._pluralizedResourceName()}`;
}

View file

@ -1,30 +1,28 @@
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
static targets = ["replacementField", "submitButton"];
static classes = ["dangerousAction", "safeAction"];
static targets = [
"replacementField",
"destructiveSubmitButton",
"safeSubmitButton",
];
static values = {
submitTextWhenReplacing: String,
submitTextWhenNotReplacing: String,
};
updateSubmitButton() {
chooseSubmitButton() {
if (this.replacementFieldTarget.value) {
this.submitButtonTarget.value = this.submitTextWhenReplacingValue;
this.#markSafe();
this.destructiveSubmitButtonTarget.hidden = true;
this.safeSubmitButtonTarget.textContent =
this.submitTextWhenReplacingValue;
this.safeSubmitButtonTarget.hidden = false;
} else {
this.submitButtonTarget.value = this.submitTextWhenNotReplacingValue;
this.#markDangerous();
this.destructiveSubmitButtonTarget.textContent =
this.submitTextWhenNotReplacingValue;
this.destructiveSubmitButtonTarget.hidden = false;
this.safeSubmitButtonTarget.hidden = true;
}
}
#markSafe() {
this.submitButtonTarget.classList.remove(...this.dangerousActionClasses);
this.submitButtonTarget.classList.add(...this.safeActionClasses);
}
#markDangerous() {
this.submitButtonTarget.classList.remove(...this.safeActionClasses);
this.submitButtonTarget.classList.add(...this.dangerousActionClasses);
}
}

View file

@ -1,7 +1,7 @@
<%# locals: (title:, back_path: nil) %>
<%= modal do %>
<div class="flex flex-col w-screen max-w-xl relative" data-controller="list-keyboard-navigation">
<%= render DialogComponent.new(open_on_load: true) do |dialog| %>
<div class="flex flex-col 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 justify-between gap-2">
<div class="flex items-center gap-2">
<% if back_path %>
@ -16,7 +16,7 @@
<span class="text-primary"><%= title %></span>
</div>
<%= icon("x", as_button: true, size: "lg", data: { action: "modal#close" }) %>
<%= icon("x", as_button: true, size: "lg", data: { action: "dialog#close" }) %>
</div>
<div class="p-2 text-subdued">
@ -45,7 +45,7 @@
</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

@ -11,7 +11,7 @@
<% 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="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">
<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)]">
<%= icon("link-2") %>
</span>
@ -21,7 +21,7 @@
<%# 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="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">
<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)]">
<%= icon("link-2") %>
</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 %>
<%= 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>
<% if category.errors.any? %>
<%= render "shared/form_errors", model: category %>

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(open_on_load: true) do |dialog| %>
<% dialog.with_header(title: t(".edit")) %>
<% dialog.with_body do %>
<%= render "form", category: @category, categories: @categories %>
<% end %>
<% end %>

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(open_on_load: true) 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(open_on_load: true) 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

@ -4,7 +4,7 @@
<div class="flex justify-between items-center">
<h2 class="font-medium text-primary"><%= t(".title") %></h2>
<%= icon("x", as_button: true, data: { action: "mousedown->modal#close" }, tabindex: "-1") %>
<%= icon("x", as_button: true, data: { action: "mousedown->dialog#close" }, tabindex: "-1") %>
</div>
<p class="text-secondary text-sm"><%= t(".description") %></p>

View file

@ -8,7 +8,7 @@
<%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %>
<%= javascript_importmap_tags %>
</head>
<body class="p-4 bg-container <%= params.dig(:lookbook, :display, :container_classes) %>">
<body class="p-4 h-full bg-container <%= params.dig(:lookbook, :display, :container_classes) %>">
<%= yield %>
</body>
</html>

View file

@ -1,5 +1,7 @@
<dialog id="confirm-dialog" data-controller="confirm-dialog" class="backdrop:bg-overlay bg-transparent m-auto p-1">
<form method="dialog" class="p-4 bg-container rounded-xl shadow-border-xs space-y-4 min-w-full lg:min-w-[300px] lg:max-w-[400px]">
<%# 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", data: { controller: "confirm-dialog" }) do %>
<form method="dialog" class="p-4 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>
@ -23,4 +25,4 @@
<% end %>
</div>
</form>
</dialog>
<% end %>

View file

@ -31,7 +31,7 @@
<%= turbo_frame_tag "drawer" %>
<%# Custom overrides for browser's confirm API %>
<%= render "layouts/shared/custom_confirm_dialog" %>
<%= 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

@ -3,7 +3,7 @@
<div>
<div class="flex justify-between mb-2 gap-4">
<h3 class="font-medium text-md">Confirm changes</h3>
<%= icon("x", as_button: true, data: { action: "mousedown->modal#close" }) %>
<%= icon("x", as_button: true, data: { action: "mousedown->dialog#close" }) %>
</div>
<div class="text-secondary text-sm">
<p>

View file

@ -1,17 +0,0 @@
<%# locals: (content:, reload_on_close: false) %>
<%= turbo_frame_tag "drawer" do %>
<dialog class="ml-auto bg-container md:shadow-border-xs md:rounded-2xl max-w-screen max-h-screen md:max-w-[480px] h-full w-full md:mt-4 md:mr-4 pt-safe focus-visible:outline-hidden"
data-controller="modal"
data-action="mousedown->modal#clickOutside"
data-modal-reload-on-close-value="<%= reload_on_close %>">
<div class="flex flex-col h-full gap-4">
<div class="flex justify-end items-center p-4">
<%= icon("x", as_button: true, data: { action: "mousedown->modal#close" }) %>
</div>
<div class="flex-1 overflow-y-auto px-4 pb-4">
<%= content %>
</div>
</div>
</dialog>
<% end %>

View file

@ -1,19 +0,0 @@
<%# locals: (content:, reload_on_close:, overflow_visible: false) -%>
<%= turbo_frame_tag "modal" do %>
<%= tag.dialog(
class: class_names(
"focus:outline-none md:m-auto bg-container rounded-none md:rounded-2xl max-w-screen max-h-screen md:max-w-max w-full h-full md:h-fit md:w-auto shadow-border-xs",
overflow_visible ? "overflow-visible" : "overflow-auto"
),
data: {
controller: "modal",
action: "mousedown->modal#clickOutside",
modal_reload_on_close_value: reload_on_close
}
) do %>
<div class="flex flex-col h-full md:h-auto mt-safe">
<%= content %>
</div>
<% end %>
<% end %>

View file

@ -1,18 +0,0 @@
<%# locals: (title:, content:, subtitle: nil, overflow_visible: false) %>
<%= modal overflow_visible: overflow_visible do %>
<article class="mx-auto w-full p-4 space-y-4 md:min-w-[450px]">
<div class="space-y-2">
<header class="flex justify-between items-center">
<h2 class="font-medium text-primary"><%= title %></h2>
<%= icon("x", as_button: true, data: { action: "mousedown->modal#close" }) %>
</header>
<% if subtitle.present? %>
<%= tag.p subtitle, class: "text-secondary font-light" %>
<% end %>
</div>
<%= content %>
</article>
<% end %>

View file

@ -1,30 +1,26 @@
<div data-controller="modal" data-modal-open-value="true" class="h-full flex items-center justify-center bg-container/90" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="w-[400px] rounded-xl relative overflow-hidden">
<div class="bg-container shadow-border-xs rounded-xl relative z-10">
<div class="rounded-xl" style="background-image: url('<%= asset_path("maybe-plus-background.svg") %>'); background-size: cover; background-position: center top;">
<div class="text-center rounded-xl" style="background-image: linear-gradient(to bottom, rgba(197,161,119,0.15) 0%, rgba(255,255,255,0.8) 30%, white 40%);">
<div class="p-4 pt-2 rounded-xl">
<div class="flex justify-center">
<%= image_tag "maybe-plus-logo.png", class: "w-20" %>
</div>
<%= render DialogComponent.new(open_on_load: true) do |dialog| %>
<div class="rounded-xl" style="background-image: url('<%= asset_path("maybe-plus-background.svg") %>'); background-size: cover; background-position: center top;">
<div class="text-center rounded-xl" style="background-image: linear-gradient(to bottom, rgba(197,161,119,0.15) 0%, rgba(255,255,255,0.8) 30%, white 40%);">
<div class="p-4 pt-2 rounded-xl">
<div class="flex justify-center">
<%= image_tag "maybe-plus-logo.png", class: "w-20" %>
</div>
<h2 class="font-medium text-primary mb-2">Join Maybe+</h2>
<h2 class="font-medium text-primary mb-2">Join Maybe+</h2>
<div class="text-secondary text-sm space-y-4 mb-5">
<p>Nobody likes paywalls, but we need feedback from users willing to pay for Maybe. </p>
<div class="text-secondary text-sm space-y-4 mb-5">
<p>Nobody likes paywalls, but we need feedback from users willing to pay for Maybe. </p>
<p>To continue using the app, please subscribe. In this early beta testing phase, we require that you upgrade within one hour to claim your spot.</p>
</div>
<p>To continue using the app, please subscribe. In this early beta testing phase, we require that you upgrade within one hour to claim your spot.</p>
</div>
<%= render LinkComponent.new(
<%= render LinkComponent.new(
text: "Upgrade to Maybe+",
href: new_subscription_path,
variant: "primary",
full_width: true
) %>
</div>
</div>
</div>
</div>
</div>
</div>
<% end %>

View file

@ -1,55 +1,25 @@
<%= turbo_frame_tag "bulk_transaction_edit_drawer" do %>
<dialog data-controller="modal"
data-action="mousedown->modal#clickOutside"
class="bg-container shadow-border-xs rounded-2xl max-h-[calc(100vh-32px)] h-full max-w-[480px] w-full mt-4 mr-4 ml-auto">
<%= render DialogComponent.new(variant: "drawer", frame: "bulk_transaction_edit_drawer", open_on_load: true) do |dialog| %>
<% dialog.with_header(title: "Edit transactions", data: { bulk_select_target: "bulkEditDrawerHeader" }) %>
<% dialog.with_body do %>
<%= styled_form_with url: transactions_bulk_update_path, scope: "bulk_update", class: "h-full", data: { turbo_frame: "_top" } do |form| %>
<div class="flex h-full flex-col justify-between p-4 gap-4">
<div>
<div class="flex h-9 items-center justify-end">
<%= icon("x", as_button: true, data: { action: "mousedown->modal#close" }) %>
</div>
<div class="flex flex-col overflow-scroll">
<div>
<header class="mb-4 space-y-1">
<h3 class="text-2xl font-medium" data-bulk-select-target="bulkEditDrawerTitle">
Edit transactions
</h3>
</header>
<div class="space-y-2">
<%= render DisclosureComponent.new(title: "Overview", open: true) do %>
<div class="pb-6 space-y-2">
<%= form.date_field :date, label: "Date", max: Date.current %>
</div>
<% end %>
<%= render DisclosureComponent.new(title: "Details", open: true) do %>
<div class="space-y-2">
<%= form.collection_select :category_id, Current.family.categories.alphabetically, :id, :name, { prompt: "Select a category", label: "Category", class: "text-subdued" } %>
<%= form.collection_select :merchant_id, Current.family.merchants.alphabetically, :id, :name, { prompt: "Select a merchant", label: "Merchant", class: "text-subdued" } %>
<%= form.select :tag_ids, Current.family.tags.alphabetically.pluck(:name, :id), { include_blank: "None", multiple: true, label: "Tags", container_class: "h-40" } %>
<%= form.text_area :notes, label: "Notes", placeholder: "Enter a note that will be applied to selected transactions", rows: 5 %>
</div>
<% end %>
</div>
</div>
</div>
<% dialog.with_section(title: "Overview", open: true) do %>
<div class="pb-6 space-y-2">
<%= form.date_field :date, label: "Date", max: Date.current %>
</div>
<% end %>
<div class="flex justify-end items-center gap-2">
<%= render LinkComponent.new(
text: "Cancel",
variant: "ghost",
href: transactions_path
) %>
<%= render ButtonComponent.new(
text: "Save",
data: { "bulk-select-scope-param": "bulk_update", action: "bulk-select#submitBulkRequest" }
) %>
<% dialog.with_section(title: "Transactions", open: true) do %>
<div class="space-y-2">
<%= form.collection_select :category_id, Current.family.categories.alphabetically, :id, :name, { prompt: "Select a category", label: "Category", class: "text-subdued" } %>
<%= form.collection_select :merchant_id, Current.family.merchants.alphabetically, :id, :name, { prompt: "Select a merchant", label: "Merchant", class: "text-subdued" } %>
<%= form.select :tag_ids, Current.family.tags.alphabetically.pluck(:name, :id), { include_blank: "None", multiple: true, label: "Tags", container_class: "h-40" } %>
<%= form.text_area :notes, label: "Notes", placeholder: "Enter a note that will be applied to selected transactions", rows: 5 %>
</div>
</div>
<% end %>
<% end %>
</dialog>
<% end %>
<% end %>
<% dialog.with_action(cancel_action: true, text: "Cancel", variant: "ghost") %>
<% dialog.with_action(text: "Save", data: { bulk_select_scope_param: "bulk_update", action: "bulk-select#submitBulkRequest" }) %>
<% end %>

View file

@ -0,0 +1,46 @@
class DialogComponentPreview < ViewComponent::Preview
# @param show_overflow toggle
def modal(show_overflow: false)
render DialogComponent.new(variant: "modal", open_on_load: true) do |dialog|
dialog.with_header(title: "Sample modal title")
dialog.with_body do
"Welcome to Maybe! This is some test modal content."
end
dialog.with_action(cancel_action: true, text: "Cancel", variant: "outline")
dialog.with_action(text: "Submit")
if show_overflow
content_tag(:div, class: "p-4 font-semibold h-[800px] bg-surface-inset") do
"Example of overflow content"
end
end
end
end
# @param show_overflow toggle
def drawer(show_overflow: false)
render DialogComponent.new(variant: "drawer", open_on_load: true) do |dialog|
dialog.with_header(title: "Drawer title")
dialog.with_body do
dialog.with_section(title: "Section 1", open: true) do
content_tag(:div, "Section 1 content", class: "p-2")
end
dialog.with_section(title: "Section 2", open: true) do
content_tag(:div, "Section 2 content", class: "p-2")
end
end
dialog.with_action(text: "Example action")
if show_overflow
content_tag(:div, class: "p-4 font-semibold h-[800px] bg-surface-inset") do
"Example of overflow content"
end
end
end
end
end

View file

@ -3,11 +3,11 @@ class DisclosureComponentPreview < ViewComponent::Preview
# @param align select ["left", "right"]
def default(align: "right")
render DisclosureComponent.new(title: "Title", align: align, open: true) do |disclosure|
disclosure.with_summary_content do
disclosure.with_summary_content do
content_tag(:p, "$200.25", class: "text-xs font-mono font-medium")
end
content_tag(:p, "Sample disclosure content", class: "text-sm")
content_tag(:p, "Sample disclosure content", class: "text-sm")
end
end
end