Packages
petal_components
1.2.10
4.7.0
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
import PetalComponents.Helpers
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 :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 :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)}
{@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={hide_modal(@close_modal_target, @id)}
phx-window-keydown={hide_modal(@close_modal_target, @id)}
phx-key="escape"
>
<!-- Header -->
<div class="pc-modal__header">
<div class="pc-modal__header__container">
<div class="pc-modal__header__text">
<%= @title %>
</div>
<button
phx-click={hide_modal(@close_modal_target, @id)}
class="pc-modal__header__button"
>
<div class="sr-only">Close</div>
<svg class="pc-modal__header__close-svg">
<path d="M7.95 6.536l4.242-4.243a1 1 0 111.415 1.414L9.364 7.95l4.243 4.242a1 1 0 11-1.415 1.415L7.95 9.364l-4.243 4.243a1 1 0 01-1.414-1.415L6.536 7.95 2.293 3.707a1 1 0 011.414-1.414L7.95 6.536z" />
</svg>
</button>
</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
def hide_modal(close_modal_target \\ nil, id \\ "modal") do
js =
%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: "#modal", transition: {"block", "block", "hidden"})
|> JS.remove_class("overflow-hidden", to: "body")
if close_modal_target do
JS.push(js, "close_modal", target: close_modal_target)
else
JS.push(js, "close_modal")
end
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
build_class([max_width_class, base_classes, custom_classes])
end
end