Packages
petal_components
1.9.1
4.6.2
4.6.1
4.6.0
4.5.0
4.4.0
4.3.0
4.2.1
4.2.0
4.1.2
4.1.1
4.1.0
4.0.12
4.0.11
4.0.10
4.0.6
4.0.5
4.0.4
4.0.3
4.0.1
3.2.2
3.2.1
3.2.0
3.1.0
3.0.2
3.0.1
3.0.0
2.9.3
2.9.2
retired
2.9.1
retired
2.9.0
retired
2.8.4
2.8.3
2.8.2
2.8.1
2.8.0
2.7.4
2.7.3
2.7.2
2.7.1
2.7.0
2.6.1
2.6.0
2.5.2
2.5.1
2.5.0
2.4.3
2.4.2
2.4.1
2.4.0
2.3.0
2.2.1
2.2.0
2.1.2
2.1.1
2.1.0
2.0.6
2.0.5
2.0.4
2.0.3
2.0.2
2.0.1
2.0.0
1.9.3
1.9.2
1.9.1
1.9.0
1.8.0
1.7.1
1.7.0
1.6.2
1.6.1
1.6.0
1.5.5
1.5.4
1.5.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.0
1.2.14
1.2.13
1.2.12
1.2.11
1.2.10
1.2.9
1.2.8
1.2.7
1.2.6
1.2.5
1.2.4
1.2.3
1.2.2
1.2.1
1.2.0
1.1.6
1.1.5
1.1.4
1.1.3
1.1.2
1.1.1
1.1.0
1.0.8
1.0.7
1.0.6
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
0.19.10
0.19.9
0.19.8
0.19.7
0.19.6
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.5
0.18.4
0.18.3
0.18.2
0.18.1
0.18.0
0.17.7
0.17.6
0.17.5
0.17.4
0.17.3
0.17.2
0.17.1
0.17.0
0.16.0
0.15.0
0.14.1
0.14.0
0.13.7
0.13.6
0.13.5
0.13.4
0.13.3
0.13.2
0.13.1
0.13.0
0.12.0
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.8
0.10.7
0.10.6
0.10.5
0.10.4
0.10.3
0.10.2
0.10.1
0.10.0
0.9.3
0.9.2
0.9.1
0.9.0
0.8.0
0.7.0
0.6.1
0.6.0
0.5.1
0.5.0
0.4.0
0.3.2
0.3.1
0.3.0
0.2.2
0.2.1
0.2.0
0.1.0
Shadcn-style Phoenix LiveView components that AI assistants can actually use. Pair with the MCP server so AI coding tools can inspect the real component API.
Current section
Files
Jump to
Current section
Files
lib/petal_components/modal.ex
defmodule PetalComponents.Modal do
use Phoenix.Component
alias Phoenix.LiveView.JS
attr :id, :string, default: "modal", doc: "modal id"
attr :hide, :boolean, default: false, doc: "modal is hidden"
attr :title, :string, default: nil, doc: "modal title"
attr :class, :string, default: nil, doc: "modal class"
attr :close_modal_target, :string,
default: nil,
doc:
"close_modal_target allows you to target a specific live component for the close event to go to. eg: close_modal_target={@myself}"
attr :close_on_click_away, :boolean,
default: true,
doc: "whether the modal should close when a user clicks away"
attr :close_on_escape, :boolean,
default: true,
doc: "whether the modal should close when a user hits escape"
attr :hide_close_button, :boolean,
default: false,
doc: "whether or not the modal should have a close button in the header"
attr :on_cancel, JS,
default: JS.exec("data-cancel-default"),
doc:
"a JS function to execute when the modal is closed. Defaults to pushing close_modal event"
attr :max_width, :string,
default: "md",
values: ["sm", "md", "lg", "xl", "2xl", "full"],
doc: "modal max width"
attr :rest, :global
slot :inner_block, required: false
def modal(assigns) do
assigns =
assigns
|> assign(:classes, get_classes(assigns))
~H"""
<div
id={@id}
phx-mounted={!@hide && show_modal(@id)}
phx-remove={hide_modal(@id)}
data-cancel={JS.exec(@on_cancel, "phx-remove")}
data-cancel-default={push_close_modal_event(@close_modal_target)}
{@rest}
class="hidden pc-modal"
>
<div class="pc-modal__overlay" aria-hidden="true"></div>
<div class="pc-modal__wrapper" role="dialog" aria-modal="true">
<div
class={@classes}
phx-click-away={@close_on_click_away && JS.exec("data-cancel", to: "##{@id}")}
phx-window-keydown={@close_on_escape && JS.exec("data-cancel", to: "##{@id}")}
phx-key="escape"
>
<!-- Header -->
<div class="pc-modal__header">
<div class="pc-modal__header__container">
<div class="pc-modal__header__text">
<%= @title %>
</div>
<%= unless @hide_close_button do %>
<button
type="button"
phx-click={JS.exec("data-cancel", to: "##{@id}")}
class="pc-modal__header__button"
>
<div class="sr-only">Close</div>
<Heroicons.x_mark class="pc-modal__header__close-svg" />
</button>
<% end %>
</div>
</div>
<!-- Content -->
<div class="pc-modal__content">
<%= render_slot(@inner_block) %>
</div>
</div>
</div>
</div>
"""
end
# The live view that calls <.modal> will need to handle the "close_modal" event. eg:
# def handle_event("close_modal", _, socket) do
# {:noreply, push_patch(socket, to: Routes.moderate_users_path(socket, :index))}
# end
defp push_close_modal_event(close_modal_target) do
if close_modal_target do
JS.push(%JS{}, "close_modal", target: close_modal_target)
else
JS.push(%JS{}, "close_modal")
end
end
def hide_modal(id \\ "modal") do
%JS{}
|> JS.hide(
to: "##{id} .pc-modal__overlay",
transition: {"transition-all transform ease-in duration-200", "opacity-100", "opacity-0"}
)
|> JS.hide(
to: "##{id} .pc-modal__box",
time: 200,
transition:
{"transition-all transform ease-in duration-200",
"opacity-100 translate-y-0 sm:scale-100",
"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"}
)
|> JS.hide(to: "##{id}", transition: {"block", "block", "hidden"})
|> JS.remove_class("overflow-hidden", to: "body")
end
# We are unsure of what the best practice is for using this.
# Open to suggestions/PRs
def show_modal(js \\ %JS{}, id) do
js
|> JS.show(to: "##{id}")
|> JS.show(
to: "##{id} .pc-modal__overlay",
transition: {"transition-all transform ease-out duration-300", "opacity-0", "opacity-100"}
)
|> JS.show(
to: "##{id} .pc-modal__box",
transition:
{"transition-all transform ease-out duration-300",
"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95",
"opacity-100 translate-y-0 sm:scale-100"}
)
|> JS.add_class("overflow-hidden", to: "body")
|> JS.focus_first(to: "##{id} .pc-modal__box")
end
defp get_classes(assigns) do
opts = %{
max_width: assigns[:max_width] || "md",
class: assigns[:class] || ""
}
base_classes = "pc-modal__box"
max_width_class = "pc-modal__box--#{opts.max_width}"
custom_classes = opts.class
[max_width_class, base_classes, custom_classes]
end
end