Packages
petal_components
4.0.6
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/slide_over.ex
defmodule PetalComponents.SlideOver do
use Phoenix.Component
alias Phoenix.LiveView.JS
import PetalComponents.Helpers, only: [compose_js: 2]
attr :id, :string, default: "slide-over"
attr(:origin, :string,
default: "right",
values: ["left", "right", "top", "bottom"],
doc: "slideover point of origin"
)
attr(:close_slide_over_target, :string,
default: nil,
doc:
"close_slide_over_target allows you to target a specific live component for the close event to go to. eg: close_slide_over_target={@myself}"
)
attr(:close_on_click_away, :boolean,
default: true,
doc: "whether the slideover should close when a user clicks away"
)
attr(:close_on_escape, :boolean,
default: true,
doc: "whether the slideover should close when a user hits escape"
)
attr(:title, :string, default: nil, doc: "slideover title")
attr(:max_width, :string,
default: "md",
values: ["sm", "md", "lg", "xl", "2xl", "full"],
doc: "sets container max-width"
)
attr :on_open, JS, default: %JS{}, doc: "additional JS commands to run when the slide over opens"
attr :on_close, JS,
default: %JS{},
doc: "additional JS commands to run when the slide over closes"
attr(:class, :any, default: nil, doc: "CSS class")
attr(:hide, :boolean, default: false, doc: "slideover is hidden")
attr(:rest, :global)
slot(:inner_block, required: false)
def slide_over(assigns) do
~H"""
<div
{@rest}
phx-mounted={!@hide && show_slide_over(@on_open, @origin, @id)}
phx-remove={compose_js(@on_close, hide_slide_over(@origin, @id, @close_slide_over_target))}
class="hidden pc-slide-over"
id={@id}
>
<div id={"#{@id}-overlay"} class="hidden pc-slideover__overlay" aria-hidden="true"></div>
<div
class={["pc-slideover__wrapper", get_margin_classes(@origin), @class]}
role="dialog"
aria-label="slide-over-content-wrapper"
aria-modal="true"
>
<div
id={"#{@id}-content"}
class={get_classes(@max_width, @origin, @class)}
phx-click-away={
@close_on_click_away &&
compose_js(@on_close, hide_slide_over(@origin, @id, @close_slide_over_target))
}
phx-window-keydown={
@close_on_escape &&
compose_js(@on_close, hide_slide_over(@origin, @id, @close_slide_over_target))
}
phx-key="escape"
>
<!-- Header -->
<div class="pc-slideover__header">
<div class="pc-slideover__header__container">
<div :if={@title} class="pc-slideover__header__text">
{@title}
</div>
<button
type="button"
phx-click={compose_js(@on_close, hide_slide_over(@origin, @id, @close_slide_over_target))}
class="pc-slideover__header__button"
>
<div class="sr-only">Close</div>
<svg class="pc-slideover__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-slideover__content">
{render_slot(@inner_block)}
</div>
</div>
</div>
</div>
"""
end
def show_slide_over(origin, id) when is_binary(origin) and is_binary(id),
do: show_slide_over(%JS{}, origin, id)
def show_slide_over(origin) when is_binary(origin),
do: show_slide_over(%JS{}, origin, "slide-over")
def show_slide_over(js, origin, id) do
{start_class, end_class} = get_transition_classes(origin)
js
|> JS.show(to: "##{id}")
|> JS.show(
to: "##{id}-overlay",
time: 300,
transition: {"transition-all transform ease-out duration-300", "opacity-0", "opacity-100"}
)
|> JS.show(
to: "##{id}-content",
time: 300,
transition: {"transition-all transform ease-out duration-300", start_class, end_class}
)
|> JS.add_class("overflow-hidden", to: "body")
|> JS.focus_first(to: "##{id}-content")
end
# The live view that calls <.slide_over> will need to handle the "close_slide_over" event. eg:
# def handle_event("close_slide_over", _, socket) do
# {:noreply, push_patch(socket, to: Routes.moderate_users_path(socket, :index))}
# end
def hide_slide_over(origin, id \\ "slide-over", close_slide_over_target \\ nil) do
{end_class, start_class} = get_transition_classes(origin)
js =
JS.remove_class("overflow-hidden", to: "body")
|> JS.hide(
transition: {"ease-in duration-200", "opacity-100", "opacity-0"},
to: "##{id}-overlay"
)
|> JS.hide(
transition: {"ease-in duration-200", start_class, end_class},
to: "##{id}-content"
)
|> JS.hide(to: "##{id}", transition: {"duration-200", "", ""})
if close_slide_over_target do
JS.push(js, "close_slide_over", target: close_slide_over_target)
else
JS.push(js, "close_slide_over")
end
end
defp get_transition_classes(origin) do
case origin do
"left" -> {"-translate-x-full", "translate-x-0"}
"right" -> {"translate-x-full", "translate-x-0"}
"top" -> {"-translate-y-full", "translate-y-0"}
"bottom" -> {"translate-y-full", "translate-y-0"}
end
end
defp get_classes(max_width, origin, class) do
base_classes = "hidden pc-slideover__box"
slide_over_classes =
case origin do
"left" -> "fixed left-0 inset-y-0 transform -translate-x-full"
"right" -> "fixed right-0 inset-y-0 transform translate-x-full"
"top" -> "fixed inset-x-0 top-0 transform -translate-y-full"
"bottom" -> "fixed inset-x-0 bottom-0 transform translate-y-full"
end
max_width_class =
case origin do
x when x in ["left", "right"] ->
"pc-slideover__box--#{max_width}"
x when x in ["top", "bottom"] ->
""
end
custom_classes = class
[slide_over_classes, max_width_class, base_classes, custom_classes]
end
defp get_margin_classes(margin) do
case margin do
"left" -> "mr-10"
"right" -> "ml-10"
"top" -> "mb-10"
"bottom" -> "mt-10"
end
end
end