mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-09 07:25:19 +02:00
Dialog replacements
This commit is contained in:
parent
779ca08d95
commit
9a67e42176
27 changed files with 344 additions and 253 deletions
|
@ -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;
|
||||
}
|
||||
|
|
38
app/components/dialog_component.html.erb
Normal file
38
app/components/dialog_component.html.erb
Normal 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>
|
93
app/components/dialog_component.rb
Normal file
93
app/components/dialog_component.rb
Normal 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
|
|
@ -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
|
|
@ -22,4 +22,4 @@
|
|||
<div class="mt-2">
|
||||
<%= content %>
|
||||
</div>
|
||||
</details>
|
||||
</details>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
)
|
||||
|
|
|
@ -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()}`;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %>
|
||||
|
|
|
@ -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 %>
|
||||
|
|
|
@ -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 %>
|
||||
|
|
|
@ -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 %>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %>
|
|
@ -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? %>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %>
|
|
@ -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 %>
|
|
@ -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 %>
|
|
@ -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 %>
|
|
@ -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 %>
|
46
test/components/previews/dialog_component_preview.rb
Normal file
46
test/components/previews/dialog_component_preview.rb
Normal 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
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue